Symfony UX Live Component: Reactive PHP Components
AI generated
SF
{ }
Symfony · UX · Live Component · PHP · Reactive
Symfony UX Live Component:
Reactive PHP components without a JavaScript framework

Search fields that load results while the user types. Forms that validate fields in real time. Cart components that react to clicks without a page reload. All of this with PHP and Twig. Symfony UX Live Component makes PHP components reactive without needing React or Vue.

18 min read LiveProp · LiveAction · Live search · Real-time validation Symfony 7.x · symfony/ux-live-component · PHP 8.3+

1. The concept: reactive PHP components

The idea behind Symfony UX Live Component is that PHP classes can become reactive: when a component property changes, because a user types into an input field or flips a switch, the browser automatically sends a server request, the component re-renders with the new state, and the DOM is updated with the result. That sounds like React or Vue, but it works entirely server side: the component is a PHP class, the template is Twig, and rendering happens on the server.

The symfony/ux-live-component package builds on the UX TwigComponent package and adds a reactivity layer on top of it. The Stimulus controller in the browser watches input fields annotated with data-model and sends an AJAX request to an internal endpoint whenever they change. The server re-renders the component and returns HTML, which gets merged into the page via Morphdom DOM diffing. Morphdom tries to change the DOM as little as possible, much like React's virtual DOM, but rendered server side. That preserves browser state such as scroll position, cursor position in input fields and CSS transitions during the update.

2. Installation and basic configuration

Installing symfony/ux-live-component requires symfony/ux-twig-component, which it brings along as a dependency. The Flex recipe registers the bundle, adds the Stimulus controller JavaScript file to the import map, and creates an internal routing entry that Live Component requests use. With AssetMapper, no further step is needed afterwards. The Stimulus controller is active right away.

For asset configuration, make sure the Live Component package is registered in AssetMapper: bin/console debug:asset should show @symfony/ux-live-component. The internal routing uses the path /_components, which must be correctly configured in the security firewall. If the application uses sessions for authentication, Live Component requests work automatically with the existing session. For stateless API applications with JWT authentication, the security configuration needs to be adjusted so the /_components path uses the right authentication method.


<?php

declare(strict_types=1);

namespace App\Twig\Components;

use App\Repository\ProductRepository;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;

/**
 * Live search component that reacts to user input in real time.
 * Re-renders automatically whenever searchQuery changes.
 */
#[AsLiveComponent]
final class ProductSearch
{
    use DefaultActionTrait;

    /** @var string The search query, automatically synced from the browser input field */
    #[LiveProp(writable: true)]
    public string $searchQuery = '';

    /** @var int Results per page, writable to allow user to change */
    #[LiveProp(writable: true)]
    public int $perPage = 10;

    public function __construct(
        private readonly ProductRepository $productRepository,
    ) {}

    /**
     * Returns filtered products based on the current searchQuery.
     *
     * @return Product[]
     */
    public function getProducts(): array
    {
        if (strlen($this->searchQuery) < 2) {
            return [];
        }

        return $this->productRepository->findBySearchQuery(
            query: $this->searchQuery,
            limit: $this->perPage,
        );
    }
}

3. Building your first Live Component

A Live Component is a PHP class carrying the #[AsLiveComponent] attribute. The attribute registers the class as a Stimulus controller and links it to automatic server rendering. The associated Twig template path follows the convention templates/components/ComponentName.html.twig, the same pattern used by TwigComponent. Every Live Component must import the DefaultActionTrait, which provides the default action for rendering.

In the Twig template, the component is embedded with {{ component('ProductSearch') }}. The component's own template must have a single root element. Live Component needs a stable DOM anchor for Morphdom diffing. The root element automatically receives the Stimulus controller attributes data-controller and data-live-url-value. Input fields are annotated with data-model="searchQuery", which tells Stimulus to send the corresponding LiveProp along with the new value on input. The re-render happens automatically once the request comes back, applied via Morphdom as a minimal DOM diff.

4. LiveProp: automatically synced properties

The #[LiveProp] attribute marks a PHP property as reactive. Without further parameters, the property is read only from the browser's perspective. The server can set it, but the browser cannot change it directly. With writable: true, the property becomes writable: the Stimulus JavaScript passes the value of the annotated input field to the server on every change. That means the property holds the value sent by the browser on the next render.

For more complex cases, #[LiveProp] offers the hydrateWith and dehydrateWith options: methods that serialize the property before sending it to the browser and deserialize it on receipt. This is needed for properties that are not simple scalar types, for example Doctrine entities or value objects. An entity property is dehydrated to its ID (an integer), and reloaded from the database on the next request. This protects against the browser injecting manipulated entity data. Only the ID is transmitted, and the entity is reloaded server side.

5. LiveAction: server actions without a dedicated route

The #[LiveAction] attribute marks a method of the Live Component as a callable action. The browser sends a POST request to the internal /_components endpoint, the name of the action is included, and the method is executed server side. The result is a re-render of the component. Classic use cases: adding an item to the cart, incrementing a like counter, deleting a file, all without a dedicated route, without a dedicated controller.

In the Twig template, a LiveAction is invoked with the data-action attribute: data-action="live#action" data-action-name="addToCart". Stimulus sends the request, the method is executed, and the component re-renders. For actions that should redirect to a different page after execution, the LiveAction method returns a Symfony response. A redirect response is correctly handled by Live Component as a redirect rather than as an HTML fragment. That enables the workflow: submit a form, validate on the server, re-render on failure, redirect on success.


<?php

declare(strict_types=1);

namespace App\Twig\Components;

use App\Entity\CartItem;
use App\Repository\CartRepository;
use App\Repository\ProductRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveArg;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;

/**
 * Cart component with live add/remove actions.
 * Re-renders automatically after each action.
 */
#[AsLiveComponent]
final class CartComponent
{
    use DefaultActionTrait;

    #[LiveProp]
    public int $productId = 0;

    public function __construct(
        private readonly CartRepository $cartRepository,
        private readonly ProductRepository $productRepository,
        private readonly EntityManagerInterface $em,
        private readonly RouterInterface $router,
    ) {}

    /**
     * Add a product to the cart, called from Twig via data-action="live#action".
     */
    #[LiveAction]
    public function addToCart(#[LiveArg] int $productId): void
    {
        $product = $this->productRepository->find($productId);
        if (!$product) {
            return;
        }

        $item = new CartItem();
        $item->setProduct($product);
        $item->setQuantity(1);
        $this->em->persist($item);
        $this->em->flush();

        // Component re-renders automatically after this method returns
    }

    /**
     * Remove an item and redirect to cart page.
     */
    #[LiveAction]
    public function removeAndRedirect(#[LiveArg] int $itemId): RedirectResponse
    {
        $item = $this->cartRepository->findItem($itemId);
        if ($item) {
            $this->em->remove($item);
            $this->em->flush();
        }

        return new RedirectResponse($this->router->generate('cart_show'));
    }
}

6. Live search with Doctrine and debouncing

Live search is the most common use case for Symfony UX Live Component. The search input is a plain HTML input with data-model="searchQuery". By default, Live Component triggers a server request on every keystroke. For search fields that is too frequent. You want to wait until the user pauses typing. Debouncing is controlled via the modifier system: data-model="on(input)|debounce(300ms):searchQuery" waits 300 milliseconds after the last keystroke before sending the request. This considerably reduces server load without a noticeable delay for the user.

The Doctrine repository method for the search uses a LIKE query or, for better relevance ranking, a full text search with MySQL MATCH AGAINST. For short search queries (under 2 characters) you return an empty array and show no results. That prevents pointless queries. The result template renders a list or grid of the matching products. The entire search experience, input, debounce, server request, rendering, DOM update, runs in under 100 milliseconds on local setups. For the user it feels instant.

7. Real-time validation with Symfony constraints

Real-time form validation is another strong use case for Symfony UX Live Component. Form rendering happens entirely inside the component, the properties correspond to the form fields, and #[LiveProp(writable: true)] marks them as reactive. Symfony constraint attributes on the properties (#[Assert\NotBlank], #[Assert\Email] etc.) are validated on every re-render. Errors are resolved via the Symfony validator and output in the template.

Validation feedback appears while the user is filling in the form, not only after submission. That is possible because every property change triggers a re-render, during which validation also runs. For good UX it is important to only show validation errors after a field has been left (data-model="on(blur)|live:updateModel:searchQuery") rather than already on the first keystroke. The Live Component system offers the on(blur) modifier for this, which only triggers the re-render once the field is left. Combined with Symfony constraints, this creates a professional validation experience entirely in PHP.

8. Performance: debounce, loading states and optimistic UI

Every re-render of a Live Component is an HTTP request. Three techniques matter for good performance: debouncing (delaying requests), loading states (user feedback during the request), and Morphdom-friendly templates (minimal DOM changes). We covered debouncing in section 6. Loading states are controlled via CSS classes that Stimulus sets during the request: data-loading="addClass(opacity-50)" on the root element or on specific child elements slightly fades the component out while it is loading. Separate spinner elements with data-loading="show" appear only during the request lifecycle.

Morphdom is the DOM diffing algorithm that Live Component uses. For good diffing, stable id attributes on list elements matter: <li id="product-{{ product.id }}"> lets Morphdom identify unchanged elements and leave them untouched. Without IDs, Morphdom only compares elements by their position, which can lead to unnecessary DOM changes and therefore flickering. For components that change rarely but re-render often (because another property changes), you can mark individual parts of the template with data-live-ignore. Morphdom skips these elements during diffing.

9. Live Component vs. Turbo Frames vs. Alpine.js

The three technologies have different areas of application and complement each other well. An overview helps with picking the right one.

Criterion Live Component Turbo Frames Alpine.js
Re-render triggered by Property change Link/form submit JS state change
Server-side state Yes, PHP properties No (URL parameters) No (browser only)
Live search while typing Native with debounce Not directly possible Client side only
Symfony constraints Native on re-render Via form submit Not native
Network overhead High (one request per change) Low (only on submit) None (client side)

Combining all three technologies in one project makes sense: Live Component for search fields and real-time forms, Turbo Frames for pagination and date range filters, Alpine.js for purely client-side UI elements like dropdowns and modals. The boundaries between the technologies are fluid. For concrete use cases, the comparison above helps choose the simplest tool.

Mironsoft

Symfony development, live components and reactive PHP architectures

Reactive Symfony components without a JavaScript framework?

We implement Symfony UX Live Component in your stack, from live search and real-time validation through LiveAction patterns to performance optimization with debouncing and loading states.

Live components

LiveProp and LiveAction for reactive search fields, carts and real-time forms in Symfony

Real-time validation

Symfony constraints in Live Components for instant validation feedback while typing

UX optimization

Debouncing, loading states and Morphdom-friendly templates for a smooth user experience

10. Summary

Symfony UX Live Component makes PHP classes reactive. Properties marked with #[LiveProp(writable: true)] are automatically synced with the browser on user actions and trigger a server re-render. LiveActions enable server-side operations without a dedicated route or controller. Real-time validation via Symfony constraints works automatically on every re-render. Live search with a Doctrine repository and debouncing is implemented in a few lines of PHP and Twig, without writing any JavaScript.

It is worth using where Turbo Frames are too coarse and Alpine.js is too client side: forms that need server-side state, search fields that trigger database queries, and complex interactions that need to talk to Symfony services directly. Live Component, Turbo Frames and Alpine.js complement each other as layers: Turbo for navigation, Live Component for reactive areas, Alpine.js for purely visual interactions. PHP teams gain a complete reactive toolkit without a JavaScript framework.

Symfony UX Live Component: The essentials at a glance

LiveProp

#[LiveProp(writable: true)] syncs properties between browser and server. When the value changes, Stimulus automatically triggers a re-render. No manual AJAX.

LiveAction

#[LiveAction] turns methods into server actions without a dedicated route. Invoked via data-action="live#action" in the Twig template, followed by an automatic re-render.

Debouncing

data-model="on(input)|debounce(300ms):searchQuery" delays re-renders on keystrokes by 300 ms, considerably reducing server load for live search.

Loading states

data-loading="addClass(opacity-50)" shows loading feedback during server requests. data-loading="show" for spinner elements, data-live-ignore for stable DOM areas.

11. FAQ: Symfony UX Live Component and reactive PHP components

1What is Symfony UX Live Component?
Makes PHP classes reactive without a JavaScript framework. LiveProp syncs properties between browser and server, LiveActions enable server-side operations without a dedicated route, followed by an automatic re-render.
2Live Component vs. Turbo Frames?
Turbo Frames: triggered by link/submit, no server-side state. Live Component: automatic on property changes, PHP properties as server-side state, ideal for real-time search.
3LiveProp vs. a normal property?
#[LiveProp(writable: true)] is visible to and syncable by Stimulus. A normal property is private to the server. Stimulus ignores it entirely.
4Live search with Doctrine?
Search field with data-model="on(input)|debounce(300ms):searchQuery". Component method calls the repository. Twig iterates over the results. Re-render happens automatically with a 300ms debounce.
5Implementing real-time validation?
Symfony constraint attributes on the LiveProp properties. The validator runs on every re-render. Errors are available in the Twig template. on(blur) modifier for validation only after leaving the field.
6Why debouncing?
Without debounce: one request per keystroke. With |debounce(300ms): one request 300ms after the pause, reducing server load from 10+ down to 1 request per search.
7Configuring loading states?
data-loading="addClass(opacity-50)" on the root. data-loading="show" for spinner elements. Stimulus sets and removes them automatically. No JavaScript required.
8Database operations in LiveActions?
Yes. Full dependency injection, EntityManager, services and repositories are directly injectable. Automatic re-render after execution with the current database state.
9What is Morphdom?
DOM diffing algorithm. Compares old and new HTML, changes only the differences. Preserves cursor position, scroll position and CSS transitions. Stable id attributes improve diff quality.
10Securing LiveActions?
Full Symfony security integration. is_granted() and #[IsGranted] work normally in LiveActions. The /_components path must be configured in the Symfony firewall.