Try Live
Add Docs
Rankings
Pricing
Docs
Install
Install
Docs
Pricing
More...
More...
Try Live
Rankings
Enterprise
Create API Key
Add Docs
Symfony UX Live Component
https://github.com/symfony/ux-live-component
Admin
Symfony UX Live Component enables automatic frontend updates for Twig components as users interact
...
Tokens:
26,047
Snippets:
192
Trust Score:
9.3
Update:
5 months ago
Context
Skills
Chat
Benchmark
85.5
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# Symfony UX Live Components ## Introduction Symfony UX Live Components is a powerful PHP library that enables automatic frontend updates for Twig components as users interact with them, inspired by Laravel Livewire and Phoenix LiveView. The bundle integrates seamlessly with Symfony's TwigComponent library to create reactive, stateful components that update without full page refreshes. Components maintain their state through LiveProps, which can be serialized and sent to the frontend, then hydrated back on the server during AJAX re-renders. The library provides a sophisticated system for building interactive UIs with minimal JavaScript, handling form submissions, validation, event broadcasting between components, and deferred/lazy loading. It includes TypeScript frontend controllers using Stimulus, automatic model binding for form inputs, lifecycle hooks for component hydration/dehydration, and seamless integration with Symfony forms and Doctrine entities. The architecture supports parent-child component relationships, URL-bound state, and polling for real-time updates. ## Core APIs and Functions ### Creating a Basic Live Component Define a live component using the `#[AsLiveComponent]` attribute with reactive properties marked by `#[LiveProp]`. ```php <?php namespace App\Component; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait; #[AsLiveComponent('todo_list')] final class TodoListComponent { use DefaultActionTrait; #[LiveProp(writable: true)] public string $searchTerm = ''; #[LiveProp] public array $items = []; public function mount(): void { $this->items = ['Buy groceries', 'Write code', 'Deploy app']; } } ``` ### Live Component Template with Model Binding Create a Twig template that automatically updates on user interaction using `data-model` attributes. ```twig <div {{ attributes }}> <input type="text" data-model="searchTerm" placeholder="Search todos..." > <ul> {% for item in items %} {% if searchTerm == '' or item|lower contains searchTerm|lower %} <li>{{ item }}</li> {% endif %} {% endfor %} </ul> </div> ``` ### Component with Actions Define interactive actions using `#[LiveAction]` attribute for user-triggered operations. ```php <?php namespace App\Component; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveAction; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait; #[AsLiveComponent('counter')] final class CounterComponent { use DefaultActionTrait; #[LiveProp(writable: true)] public int $count = 0; #[LiveAction] public function increment(): void { $this->count++; } #[LiveAction] public function decrement(): void { $this->count--; } #[LiveAction] public function reset(): void { $this->count = 0; } } ``` ```twig <div {{ attributes }}> <h2>Count: {{ count }}</h2> <button data-action="live#action" data-live-action-param="increment">+</button> <button data-action="live#action" data-live-action-param="decrement">-</button> <button data-action="live#action" data-live-action-param="reset">Reset</button> </div> ``` ### Form Integration with ComponentWithFormTrait Integrate Symfony forms into live components with automatic validation and submission handling. ```php <?php namespace App\Component; use App\Entity\User; use App\Form\UserFormType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormInterface; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveAction; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\ComponentWithFormTrait; use Symfony\UX\LiveComponent\DefaultActionTrait; #[AsLiveComponent('user_form')] class UserFormComponent extends AbstractController { use ComponentWithFormTrait; use DefaultActionTrait; #[LiveProp] public User $user; protected function instantiateForm(): FormInterface { return $this->createForm(UserFormType::class, $this->user); } #[LiveAction] public function save(): void { $this->submitForm(); $form = $this->getForm(); if ($form->isValid()) { $entityManager = $this->getDoctrine()->getManager(); $entityManager->persist($this->user); $entityManager->flush(); $this->addFlash('success', 'User saved successfully!'); } } } ``` ```twig <div {{ attributes }}> {{ form_start(form) }} {{ form_row(form.name) }} {{ form_row(form.email) }} <button data-action="live#action" data-live-action-param="save" > Save User </button> {{ form_end(form) }} </div> ``` ### Validation with ValidatableComponentTrait Add real-time validation to components using Symfony's validator constraints. ```php <?php namespace App\Component; use Symfony\Component\Validator\Constraints as Assert; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveAction; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait; use Symfony\UX\LiveComponent\ValidatableComponentTrait; #[AsLiveComponent('contact_form')] final class ContactFormComponent { use DefaultActionTrait; use ValidatableComponentTrait; #[LiveProp(writable: true)] #[Assert\NotBlank] #[Assert\Length(min: 3, max: 100)] public string $name = ''; #[LiveProp(writable: true)] #[Assert\NotBlank] #[Assert\Email] public string $email = ''; #[LiveProp(writable: true)] #[Assert\NotBlank] #[Assert\Length(min: 10)] public string $message = ''; #[LiveAction] public function submit(): void { $this->validate(); // Send email or save to database // ... } } ``` ```twig <div {{ attributes }}> <form> <div> <label>Name</label> <input type="text" data-model="on(blur)|name"> {% if _errors.has('name') %} <span class="error">{{ _errors.get('name').message }}</span> {% endif %} </div> <div> <label>Email</label> <input type="email" data-model="on(blur)|email"> {% if _errors.has('email') %} <span class="error">{{ _errors.get('email').message }}</span> {% endif %} </div> <div> <label>Message</label> <textarea data-model="on(blur)|message"></textarea> {% if _errors.has('message') %} <span class="error">{{ _errors.get('message').message }}</span> {% endif %} </div> <button data-action="live#action" data-live-action-param="submit"> Send Message </button> </form> </div> ``` ### LiveProp Configuration Options Configure LiveProp behavior with writable paths, custom hydration, and URL binding. ```php <?php namespace App\Component; use App\Entity\Product; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait; use Symfony\UX\LiveComponent\Metadata\UrlMapping; #[AsLiveComponent('product_search')] final class ProductSearchComponent { use DefaultActionTrait; // Fully writable from frontend #[LiveProp(writable: true)] public string $query = ''; // Bound to URL query parameter #[LiveProp(writable: true, url: true)] public int $page = 1; // Bound to URL with custom name #[LiveProp(writable: true, url: new UrlMapping(name: 'category'))] public ?string $categoryFilter = null; // Writable only for specific nested paths #[LiveProp(writable: ['price.min', 'price.max'])] public array $filters = [ 'price' => ['min' => 0, 'max' => 1000], 'inStock' => true, ]; // Custom hydration/dehydration #[LiveProp(hydrateWith: 'hydrateProduct', dehydrateWith: 'dehydrateProduct')] public ?Product $selectedProduct = null; // DateTime with specific format #[LiveProp(writable: true, format: 'Y-m-d')] public ?\DateTimeInterface $startDate = null; // Hook called after property update #[LiveProp(writable: true, onUpdated: 'onQueryChanged')] public string $searchQuery = ''; public function hydrateProduct(int $id): ?Product { return $this->productRepository->find($id); } public function dehydrateProduct(?Product $product): ?int { return $product?->getId(); } public function onQueryChanged(string $oldValue): void { $this->page = 1; // Reset to first page on new search } } ``` ### Event Broadcasting Between Components Emit and listen to events for inter-component communication. ```php <?php namespace App\Component; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveAction; use Symfony\UX\LiveComponent\Attribute\LiveListener; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait; #[AsLiveComponent('cart')] final class CartComponent { use DefaultActionTrait; #[LiveProp] public array $items = []; #[LiveListener('product:added')] public function onProductAdded(int $productId, int $quantity): void { if (isset($this->items[$productId])) { $this->items[$productId]['quantity'] += $quantity; } else { $this->items[$productId] = [ 'id' => $productId, 'quantity' => $quantity, ]; } } #[LiveListener('cart:clear')] public function clearCart(): void { $this->items = []; } } #[AsLiveComponent('product_item')] final class ProductItemComponent { use DefaultActionTrait; #[LiveProp] public int $productId; #[LiveAction] public function addToCart(): void { $this->emit('product:added', [ 'productId' => $this->productId, 'quantity' => 1, ]); } } ``` ```twig {# Product item template #} <div {{ attributes }}> <button data-action="live#action" data-live-action-param="addToCart" > Add to Cart </button> {# Or emit directly from template #} <button data-action="live#emitUp" data-live-event-param="product:added" data-live-product-id-param="{{ productId }}" data-live-quantity-param="1" > Add to Cart (Direct Emit) </button> </div> ``` ### Lifecycle Hooks Use lifecycle hooks to execute code at specific points in the component lifecycle. ```php <?php namespace App\Component; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\Attribute\PostHydrate; use Symfony\UX\LiveComponent\Attribute\PreDehydrate; use Symfony\UX\LiveComponent\Attribute\PreReRender; use Symfony\UX\LiveComponent\DefaultActionTrait; #[AsLiveComponent('user_dashboard')] final class UserDashboardComponent { use DefaultActionTrait; #[LiveProp] public array $userData = []; #[LiveProp(writable: true)] public string $filter = ''; private array $cachedResults = []; /** * Called after component is hydrated from frontend data */ #[PostHydrate] public function postHydrate(): void { // Refresh sensitive data from database after hydration $this->userData['lastLogin'] = $this->userRepository->getLastLogin(); } /** * Called before component state is serialized to frontend */ #[PreDehydrate] public function preDehydrate(): void { // Remove sensitive data before sending to frontend unset($this->userData['sensitiveField']); } /** * Called before re-rendering (only on AJAX requests) */ #[PreReRender] public function preReRender(): void { // Fetch fresh data before each re-render if ($this->filter) { $this->cachedResults = $this->searchService->search($this->filter); } } public function getFilteredResults(): array { return $this->cachedResults; } } ``` ### Frontend JavaScript Integration Access component functionality from JavaScript using the Stimulus controller. ```javascript // Import the controller import { Controller } from '@hotwired/stimulus'; import { getComponent } from '@symfony/ux-live-component'; export default class extends Controller { connect() { // Get the component instance const component = getComponent(this.element); // Listen to component events this.element.addEventListener('live:connect', (event) => { console.log('Component connected', event.detail.component); }); this.element.addEventListener('live:render:finished', (event) => { console.log('Component re-rendered'); }); // Update model programmatically component.set('searchTerm', 'New value', true); // true = should render // Call an action component.action('save', { arg1: 'value' }); // Emit events component.emit('custom:event', { data: 'value' }); component.emitUp('bubble:event', { data: 'value' }); component.emitSelf('self:event', { data: 'value' }); } } ``` ### Model Directive Modifiers Control model binding behavior with directive modifiers for debouncing and event targeting. ```twig <div {{ attributes }}> {# Update on input with default debounce #} <input type="text" data-model="search"> {# Update only on change event #} <input type="text" data-model="on(change)|email"> {# Update on blur event #} <input type="text" data-model="on(blur)|username"> {# Custom debounce (milliseconds) #} <input type="text" data-model="debounce(1000)|query"> {# No debounce #} <input type="text" data-model="norender|debounce(0)|fastInput"> {# Update without re-rendering #} <input type="text" data-model="norender|backgroundData"> {# Minimum/maximum length before triggering #} <input type="text" data-model="minlength(3)|maxlength(50)|search"> {# Numeric constraints #} <input type="number" data-model="min(0)|max(100)|age"> {# Wildcard model for forms - binds all fields automatically #} <form data-model="on(change)|*"> <input type="text" name="firstName"> <input type="text" name="lastName"> </form> </div> ``` ### Lazy and Deferred Loading Load components lazily or defer rendering for better performance. ```twig {# Lazy load - renders when scrolled into view #} <div {{ attributes.defaults({ 'data-loading': 'lazy' }) }}> {{ component('heavy_component', { data: complexData }) }} </div> {# Deferred loading - shows placeholder, then loads #} <div {{ attributes.defaults({ 'data-loading': 'defer' }) }}> Loading... </div> {# Deferred with custom placeholder #} {{ component('heavy_chart', { data: chartData }, { loading: 'defer', loading_template: 'components/_chart_loading.html.twig' }) }} {# Polling - refresh every N milliseconds #} <div {{ attributes.defaults({ 'data-poll': '5000' }) }}> Last updated: {{ "now"|date('H:i:s') }} </div> ``` ### Loading States and Indicators Add loading states to actions and model updates for better UX. ```twig <div {{ attributes }}> <form> <input type="text" data-model="search" data-loading="addClass(opacity-50)" > <button data-action="live#action" data-live-action-param="save" data-loading="addClass(loading) removeClass(ready)" > <span data-loading="hide">Save</span> <span data-loading="show" style="display:none">Saving...</span> </button> {# Disable during any component update #} <button data-loading="attr(disabled)"> Cannot click during update </button> {# Show spinner during specific action #} <div data-loading="show|action(save)" style="display:none" > <span class="spinner"></span> Saving... </div> {# Multiple loading states #} <button data-action="live#action" data-live-action-param="delete" data-loading="addClass(loading) attr(disabled)" > Delete </button> </form> </div> ``` ### Component Configuration and Routing Configure component behavior and customize routing. ```php <?php namespace App\Component; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait; // Custom route and HTTP method #[AsLiveComponent( name: 'custom_component', template: 'components/my_custom_template.html.twig', route: 'app_custom_live_route', method: 'get', urlReferenceType: UrlGeneratorInterface::ABSOLUTE_URL )] final class CustomComponent { use DefaultActionTrait; #[LiveProp] public string $data = ''; } ``` ```php // config/routes.php use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; return function (RoutingConfigurator $routes) { // Default route for all live components $routes->add('ux_live_component', '/_components/{_live_component}/{_live_action}') ->defaults([ '_live_action' => 'get', ]); // Custom route for specific component $routes->add('app_custom_live_route', '/api/live/{_live_component}/{_live_action}') ->defaults([ '_live_action' => 'get', ]); }; ``` ## Summary and Use Cases Symfony UX Live Components excels in scenarios requiring interactive user interfaces without the complexity of a full JavaScript framework. Common use cases include real-time search interfaces with instant filtering, multi-step forms with progressive validation, shopping carts that update without page refreshes, live dashboards with polling updates, and dynamic form collections that expand or contract based on user input. The library shines when building admin panels, wizard-style interfaces, inline editing functionality, and components that need to communicate through events like notification systems or collaborative features. Integration patterns typically involve server-side PHP components that extend Symfony controllers, leveraging traits like `ComponentWithFormTrait` for form handling and `ValidatableComponentTrait` for validation. Frontend integration uses Stimulus controllers with data attributes for model binding (`data-model`), action triggering (`data-action="live#action"`), and event emission (`data-action="live#emit"`). The architecture supports nested child components, URL state synchronization for bookmarkable interfaces, custom hydration strategies for complex entities, and seamless Doctrine ORM integration. Advanced patterns include implementing optimistic UI updates, handling file uploads, managing loading states with granular control, and building real-time collaborative features using event broadcasting between multiple component instances.