[LiveComponent] Support JSONResponse for LiveActions
| 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
performRequestmethod in the LiveComponent JS handler to also check forapplication/jsonin the response'sContent-Typeheader 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."));
}
}
// ...
}
📊 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.
| File | Before (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% 📈
|
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.
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.