Skip to content

[LiveComponent] Support JSONResponse for LiveActions #2967

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: 2.x
Choose a base branch
from

Conversation

xDeSwa
Copy link
Contributor

@xDeSwa xDeSwa commented Aug 1, 2025

Q A
Bug fix? no
New feature? yes
Docs? yes
Issues #
License MIT

Summary

This PR improves the LiveComponent JavaScript request handling to correctly distinguish between standard HTML LiveComponent responses and pure JSON API responses from LiveAction methods.
Specifically, it prevents error rendering in the frontend when a LiveAction returns a JsonResponse (such as for AJAX-like API interactions) by adding an additional check for the application/json content type in the response headers.

Problem

When using LiveComponent actions that return JsonResponse (e.g., for API-like operations), the LiveComponent JS handler previously attempted to render an error if the response did not contain a LiveComponent HTML payload.
This caused issues in cases where a component intentionally returns JSON (for example, on deletion or API-style actions), leading to a confusing user experience.

Solution

  • Updated the performRequest method in the LiveComponent JS handler to also check for application/json in the response's Content-Type header before triggering error rendering.
  • If the response is JSON, the frontend will not try to render a LiveComponent error, enabling custom handling in Stimulus controllers or other client code.

Example Usage

Stimulus Controller

import {Controller} from "@hotwired/stimulus";
import {getComponent} from '@symfony/ux-live-component';
import Swal from 'sweetalert2';

export default class extends Controller {
    static params = {id: Number}

    async initialize() {
        this.component = await getComponent(this.element);
    }

    postDelete(event) {
        let id = event.params.id;
        let component = this.component;

        if (id === undefined || id === 0) {
            return;
        }

        Swal.fire({
            title: "Confirmation Box",
            text: "Record will delete, are you sure?!",
            icon: "warning",
            showCancelButton: !0,
            confirmButtonColor: "#28bb4b",
            cancelButtonColor: "#f34e4e",
            confirmButtonText: "Yes, Im Sure",
            cancelButtonText: "Cancel",
        }).then(async (e) => {
            if (e.isConfirmed) {
                try {
                    const result = await component.action('deletePost', {id: id});
                    const response = JSON.parse(result.body);

                    if (response.status === "success") {
                        Swal.fire("Success", response.message, "success");
                    } else {
                        Swal.fire("Error", response.message, "error");
                    }
                } catch (err) {
                    Swal.fire("Error", err, "error");
                }
                this.component.render();
            }
        });
    }
}

Twig Example

<div {{ attributes.defaults(stimulus_controller('postactions')) }}>
    <button data-postactions-id-param="{{ post.id }}"
            data-action="click->postactions#postDelete"
            class="btn btn-danger btn-xs d-flex align-items-center justify-content-center"
            title="Delete">
        Delete
    </button>
</div>

LiveComponent Example

// src/Twig/Components/Posts.php
namespace App\Twig\Components;

// ...
use Symfony\UX\LiveComponent\Attribute\LiveAction;

class Posts
{
    // ...

    #[LiveAction]
    public function deletePost(#[LiveArg] int $id): JsonResponse
    {
        $post = $this->postRepository->find($id);
        if ($post) {
            try {
                $this->entityManager->remove($post);
                $this->entityManager->flush();
            } catch (Exception) {
                return new JsonResponse(array("status" => "error", "message" => "Post delete failed."));
            }
            return new JsonResponse(array("status" => "success", "message" => "Post deleted successfully."));
        } else {
            return new JsonResponse(array("status" => "error", "message" => "Error: Post was not found."));
        }
    }

    // ...
}

@carsonbot carsonbot added Bug Bug Fix LiveComponent Status: Needs Review Needs to be reviewed labels Aug 1, 2025
Copy link
Contributor

github-actions bot commented Aug 1, 2025

📊 Packages dist files size difference

Thanks for the PR! Here is the difference in size of the packages dist files between the base branch and the PR.
Please review the changes and make sure they are expected.

FileBefore (Size / Gzip)After (Size / Gzip)
LiveComponent
live_controller.d.ts 7.96 kB / 1.96 kB 8.22 kB+3% 📈 / 2.02 kB+3% 📈
live_controller.js 99.04 kB / 21.35 kB 99.98 kB+1% 📈 / 21.51 kB+1% 📈

Copy link
Member

@Kocal Kocal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion!

Here some majors comments that must be addressed, the actual implementation is partially broken and must be fixed.

Also, please ensure to add tests PHP-side and JS-side, as we want to be sure to not break anything.
Could you also add documentation about returning a custom Response and JsonResponse as well?

Thanks!

@carsonbot carsonbot added Status: Needs Work Additional work is needed and removed Status: Needs Review Needs to be reviewed labels Aug 3, 2025
@smnandre
Copy link
Member

smnandre commented Aug 3, 2025

After some time to think, here are my « at this moment » thoughts

  • return nothing is something we already discussed and planned but for 3.0 as it would be a major change
  • same for délétion, with custom LiveResponse helper and we would implement the délétion and events here, all this features must be easy to use for every users, especially for people not wanting to do additionnal JS
  • LiveComponent are not made for API calls and i’m not sure to understand the need here, or the real life use case
  • the core concept of LiveComponent is to render HTML and deal with state in DOM with morphing and events, I’m Not sure to see JSON as an additionnal feature but mainly I dont think we offer any interest for people if they want to do all in JS themselves.. over any exposed restish API

as much I understand the précise need you are dealing with right now, I am really Not sure this is direction we should go to.

LiveComponent is already complex for many, and if I agree we need to open more extension point, they should be only that and we cannot make promise or deal with BC afterward for them implémentation.

On the contrary I think — and i join you here — we must adress more or the « basic / regular » needs people using LiveComponent have. Including but not limited to component lifecycle (delete, update, refresh), identification, partial rendering or no render, locking or sleep, etc)

But for all this we need, imho, to stop stacking features and more keep a global / easy to understand philosophy here that would ease both maintenance and new feature dev, improve code quality testability and security.

all of this is not directly related to this PR but it is something I wanted to write down as a block for some time, in order to give clear vision of my personal state of mind here

Again: this is my own personal opinion right now and am more than open to any feedback / critique or exchange on it.

@xDeSwa
Copy link
Contributor Author

xDeSwa commented Aug 4, 2025

It would be great if we could learn about the features planned for version 3.0.

For example, I'm considering implementing something similar to what Livewire provides — like custom error handlers. Here's a use case:

window.LiveComponent.onError = (response, statusCode, component) => {
    if (statusCode === 500) {
        console.error('Server error in component:', component.name);
        return true; // continue with default error handling
    }

    return true;
};

Another example relates to dynamic behavior based on responses. While it's possible to attach a Stimulus controller to a component's root element, sometimes we need to react programmatically to the result of a LiveAction — and based on that result, decide whether to re-render the component or not.

In such cases, it would be very useful to have the ability to disable rendering from within a JsonResponse inside a LiveAction method, allowing us to use the returned data on the frontend without triggering a component re-render.

@carsonbot carsonbot added Status: Needs Review Needs to be reviewed and removed Status: Needs Work Additional work is needed labels Aug 6, 2025
@xDeSwa xDeSwa changed the title [LiveComponent] Prevent Error Rendering on JSON Response [LiveComponent] Support JSONResponse for LiveActions Aug 6, 2025
@xDeSwa xDeSwa requested a review from Kocal August 6, 2025 06:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants