ux icon indicating copy to clipboard operation
ux copied to clipboard

[LiveComponent] Support JSONResponse for LiveActions

Open xDeSwa opened this issue 5 months ago • 3 comments

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."));
        }
    }

    // ...
}

xDeSwa avatar Aug 01 '25 06:08 xDeSwa

📊 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% 📈

github-actions[bot] avatar Aug 01 '25 06:08 github-actions[bot]

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.

smnandre avatar Aug 03 '25 21:08 smnandre

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.

xDeSwa avatar Aug 04 '25 08:08 xDeSwa