Symfony UX LiveComponent for reactive server-rendered UI -- components that re-render via AJAX on user interaction, zero JavaScript required. Use when building live search, real-time filtering, dynamic forms, inline validation, dependent selects, auto-save, polling, deferred/lazy rendering, or any UI that updates itself based on user input. Code triggers: AsLiveComponent, LiveProp, LiveAction, data-model, data-loading, data-live-action-url, ComponentWithFormTrait, LiveListener, emit, defer, lazy
git clone https://github.com/smnandre/symfony-ux-skills.git--- name: live-component description: 'Symfony UX LiveComponent for reactive server-rendered UI -- components that re-render via AJAX on user interaction, zero JavaScript required. Use when building live search, real-time filtering, dynamic forms, inline validation, dependent selects, auto-save, polling, deferred/lazy rendering, or any UI that updates itself based on user input. Code triggers: AsLiveComponent, LiveProp, LiveAction, data-model, data-loading, data-live-action-url, ComponentWithFormTrait, LiveListener, emit, defer, lazy, polling. Also trigger when the user asks "how to build a search that filters as I type", "how to validate a form in real-time", "how to make a reactive component in PHP", "how to build dependent selects", "how to defer component rendering", "how to communicate between components via emit", "how to bind a form to a LiveComponent". Do NOT trigger for static reusable UI without reactivity (use twig-component), pure client-side JS behavior (use stimulus), or page-level navigation (use turbo).' license: MIT metadata: author: Simon Andre email: [email protected] url: https://smnandre.dev version: "1.2.0" --- # LiveComponent TwigComponents that re-render dynamically via AJAX. Build reactive UIs in PHP + Twig with zero JavaScript. Every user interaction triggers a server round-trip that re-renders the component and morphs the DOM. ## When to Use LiveComponent Use LiveComponent when a component's output depends on user interaction -- search results that update as you type, forms with real-time validation, filters that refine a list, anything where the UI needs to change based on user input and that change requires server-side data or logic. If the component never re-renders after initial load, use TwigComponent instead (less overhead, no AJAX). If the interaction is purely client-side (toggle, animation), use Stimulus instead. ## Installation ```bash composer require symfony/ux-live-component ``` ## Quick Reference ``` #[AsLiveComponent] Make component live (re-renderable via AJAX) #[LiveProp] State that persists across re-renders #[LiveProp(writable: true)] State that the frontend can modify #[LiveAction] Server method callable from frontend data-model="prop" Two-way bind input to LiveProp data-action="live#action" Call LiveAction on event data-loading="..." Show/hide/style elements during AJAX {{ attributes }} REQUIRED on root element (wires the Stimulus controller) ``` ## Basic Example ```php // src/Twig/Components/Counter.php namespace App\Twig\Components; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\Attribute\LiveAction; use Symfony\UX\LiveComponent\DefaultActionTrait; #[AsLiveComponent] final class Counter { use DefaultActionTrait; #[LiveProp] public int $count = 0; #[LiveAction] public function increment(): void { $this->count++; } #[LiveAction] public function decrement(): void { $this->count--; } } ``` ```twig {# templates/components/Counter.html.twig #} <div {{ attributes }}> <button data-action="live#action" data-live-action-param="decrement">-</button> <span>{{ count }}</span> <button data-action="live#action" data-live-action-param="increment">+</button> </div> ``` **Critical:** The root element must render `{{ attributes }}`. This injects the Stimulus `data-controller="live"` attribute that makes the whole system work. Without it, nothing re-renders. ## LiveProp State that persists between AJAX re-renders. Props are serialized to the frontend and sent back on every request. ### Basic Props ```php #[LiveProp] public string $query = ''; #[LiveProp] public int $page = 1; #[LiveProp] public ?User $user = null; // Entities auto-hydrate by ID ``` ### Writable Props (Two-way Binding) Only writable props can be modified from the frontend via `data-model`: ```php #[LiveProp(writable: true)] public string $search = ''; // Writable with specific fields for objects #[LiveProp(writable: ['email', 'name'])] public User $user; ``` ### URL Binding Sync a prop to a URL query parameter -- enables bookmarkable/shareable state: ```php #[LiveProp(writable: true, url: true)] public string $query = ''; // URL becomes: ?query=search+term // Custom parameter name use Symfony\UX\LiveComponent\Metadata\UrlMapping; #[LiveProp(writable: true, url: new UrlMapping(as: 'q'))] public string $query = ''; // URL becomes: ?q=search+term ``` ### Hydration Doctrine entities auto-hydrate by ID. For custom types: ```php #[LiveProp(hydrateWith: 'hydrateStatus', dehydrateWith: 'dehydrateStatus')] public Status $status; public function hydrateStatus(string $value): Status { return Status::from($value); } public function dehydrateStatus(Status $status): string { return $status->value; } ``` ## Data Binding (data-model) Bind inputs to writable LiveProps. When the input changes, the component re-renders with the new value. ```twig {# Re-render on change (default) #} <input type="text" data-model="search"> {# Debounced -- wait 300ms after last keystroke #} <input type="text" data-model="debounce(300)|search"> {# Only update on blur #} <input type="text" data-model="on(blur)|search"> {# Update model but don't re-render yet #} <input type="text" data-model="norender|search"> {# Checkbox, radio, select #} <input type="checkbox" data-model="enabled"> <select data-model="category"> <option value="1">Category 1</option> </select> ``` ### Validation Modifiers ```twig {# Only re-render when input meets criteria #} <input data-model="minlength(3)|search"> <input data-model="maxlength(100)|bio"> <input data-model="min(0)|quantity"> <input data-model="max(999)|price"> ``` ## LiveAction Server methods callable from the frontend: ```php #[LiveAction] public function save(): void { // Called via data-action="live#action" data-live-action-param="save" } #[LiveAction] public function delete(#[LiveArg] int $id): void { // With typed argument via data-live-id-param="123" } ``` ### Calling Actions from Twig ```twig {# Button click #} <button data-action="live#action" data-live-action-param="save">Save</button> {# With arguments #} <button data-action="live#action" data-live-action-param="delete" data-live-id-param="{{ item.id }}" >Delete</button> {# Form submit (prevent default) #} <form data-action="live#action:prevent" data-live-action-param="submit"> ``` ## Search Example (Complete) ```php #[AsLiveComponent] final class ProductSearch { use DefaultActionTrait; #[LiveProp(writable: true, url: true)] public string $query = ''; #[LiveProp(writable: true)] public string $category = ''; public function __construct( private readonly ProductRepository $products, ) {} public function getProducts(): array { return $this->products->search($this->query, $this->category); } } ``` ```twig <div {{ attributes }}> <input type="search" data-model="debounce(300)|query" placeholder="Search..."> <select data-model="category"> <option value="">All Categories</option> {% for cat in categories %} <option value="{{ cat.id }}">{{ cat.name }}</option> {% endfor %} </select> <div data-loading="addClass(opacity-50)"> {% for product in this.products %} <div>{{ product.name }}</div> {% endfor %} </div> </div> ``` ## Loading States Show visual feedback during AJAX re-renders: ```twig {# Add/remove class while loading #} <div data-loading="addClass(opacity-50)"> <div data-loading="removeClass(hidden)"> {# Show/hide element while loading #} <span data-loading="show">Loading...</span> <div data-loading="hide">Content</div> {# Disable button while loading #} <button data-loading="attr(disabled)">Submit</button> {# Scoped to specific action or model #} <span data-loading="action(save)|show">Saving...</span> <span data-loading="model(query)|show">Searching...</span> {# Delay before showing (avoid flicker on fast responses) #} <span data-loading="delay(300)|show">Loading...</span> ``` ## Form Integration ```php use Symfony\Component\Form\FormInterface; use Symfony\UX\LiveComponent\ComponentWithFormTrait; #[AsLiveComponent] final class RegistrationForm extends AbstractController { use DefaultActionTrait; use ComponentWithFormTrait; #[LiveProp] public ?User $initialFormData = null; protected function instantiateForm(): FormInterface { return $this->createForm(UserType::class, $this->initialFormData); } #[LiveAction] public function save(EntityManagerInterface $em): Response { $this->submitForm(); $user = $this->getForm()->getData(); $em->persist($user); $em->flush(); return $this->redirectToRoute('app_success'); } } ``` ```twig <div {{ attributes }}> {{ form_start(form, { attr: { 'data-action': 'live#action:prevent', 'data-live-action-param': 'save' } }) }} {{ form_row(form.email) }} {{ form_row(form.password) }} <button type="submit" data-loading="attr(disabled)">Register</button> {{ form_end(form) }} </div> ``` ### Real-time Validation ```twig {{ form_row(form.email, { attr: {'data-model': 'on(blur)|validatedFields'} }) }} ``` ## Component Communication ### Emit Events (Child to Parent) ```php use Symfony\UX\LiveComponent\ComponentToolsTrait; #[AsLiveComponent] final class ChildComponent { use DefaultActionTrait; use ComponentToolsTrait; #[LiveAction] public function save(): void { // ... save logic $this->emit('itemSaved', ['id' => $this->item->getId()]); } } ``` ### Listen to Events (Parent) ```php use Symfony\UX\LiveComponent\Attribute\LiveListener; #[AsLiveComponent] final class ParentComponent { use DefaultActionTrait; #[LiveListener('itemSaved')] public function onItemSaved(#[LiveArg] int $id): void { // Component re-renders automatically after this method } } ``` ### Browser Events (LiveComponent to Stimulus) ```php $this->dispatchBrowserEvent('modal:close'); ``` ```html <!-- Stimulus picks it up --> <div data-action="modal:close@window->modal#close"> ``` ## Polling Auto-refresh a component on a timer: ```twig {# Default: every 2 seconds #} <div {{ attributes }} data-poll> {# Custom interval #} <div {{ attributes }} data-poll="delay(5000)"> {# Call specific action on each poll #} <div {{ attributes }} data-poll="action(refresh)"> ``` ## Lazy / Deferred Loading ```twig {# Load component after page renders (deferred AJAX call) #} <twig:HeavyComponent defer /> {# Load when element scrolls into viewport (IntersectionObserver) #} <twig:HeavyComponent lazy /> {# Placeholder while loading #} <twig:HeavyComponent lazy> <div>Loading...</div> </twig:HeavyComponent> ``` ## Data Preservation ```twig {# Prevent re-render from modifying this subtree #} <div data-live-ignore> {# Third-party widget, contenteditable, etc. #} </div> {# Preserve specific attribute during DOM morph #} <input data-live-preserve="value"> ``` ## Computed Properties Same as TwigComponent -- `getXxx()` methods are accessible as `this.xxx`. Use `computed.xxx` for caching within a single render cycle (avoids calling the method multiple times in a loop). ```php public function getFilteredItems(): array { return array_filter($this->items, fn($i) => $i->isActive()); } ``` ```twig {# Uncached -- called each time #} {% for item in this.filteredItems %} {# Cached within this render #} {% for item in computed.filteredItems %} ``` ## Key Principles **Every interaction is a server round-trip.** LiveComponent is not a client-side framework. Each re-render sends the full component state to the server, re-executes PHP, and morphs the DOM. For high-frequency interactions (drag-and-drop, real-time drawing), use Stimulus instead. **Keep components small.** Large components with many LiveProps and complex templates are slow to re-render. Split into smaller, focused components that communicate via emit/listen. **Use `norender` and `on(blur)` to reduce requests.** Not every keystroke needs a server call. Debounce text inputs, defer binding to blur events for fields that don't need instant feedback. **`{{ attributes }}` on root element is non-negotiable.** Without it, the live behavior Stimulus controller is never attached and nothing works. ## References - **Full API** (props, actions, forms, events, all options): [references/api.md](references/api.md) - **Patterns** (search, CRUD, modals, validation, real-world examples): [references/patterns.md](references/patterns.md) - **Gotchas** (props, hydration, performance, common mistakes): [references/gotchas.md](references/gotchas.md) ## See Also - **UX Map** provides `ComponentWithMapTrait` for reactive maps inside LiveComponents. The map automatically updates when LiveProps change. - **UX Icons** work inside LiveComponent templates with no special setup -- icons re-render on each server round-trip like any other Twig markup.
1. **Install Dependencies**: Ensure Symfony UX LiveComponents are installed via: ```bash composer require symfony/ux-live-component npm install --force npm run watch ``` 2. **Create the Component**: Generate a new LiveComponent class with `#[AsLiveComponent]` attribute. Define reactive properties using `#[LiveProp]` and actions with `#[LiveAction]`. 3. **Design the Template**: Use Twig with LiveComponent directives like `data-model`, `@input.debounce`, and `init_live_component(this)`. Add loading states and error handling. 4. **Connect to Backend**: Implement server-side logic in your LiveAction methods. Use services for data fetching or validation. For forms, add `ComponentWithFormTrait`. 5. **Test and Optimize**: Use Symfony’s profiler to debug AJAX calls. Add `data-loading` attributes for visual feedback. For complex UIs, use `defer` or `lazy` for performance. Tips: - Use `writable: true` in `#[LiveProp]` for properties that can be modified by the client. - For dependent selects, chain `#[LiveAction]` calls with `data-live-action-url`. - Enable polling with `data-poll` attribute for periodic updates.
No install command available. Check the GitHub repository for manual installation instructions.
git clone https://github.com/smnandre/symfony-ux-skills/tree/main/skills/live-componentCopy the install command above and run it in your terminal.
Launch Claude Code, Cursor, or your preferred AI coding agent.
Use the prompt template or examples below to test the skill.
Adapt the skill to your specific use case and workflow.
Create a Symfony UX LiveComponent for a [COMPONENT_NAME] that updates in real-time based on user input. Use [LIVE_PROPS] for reactive properties and [LIVE_ACTIONS] for server-side handlers. Include [FORM_HANDLING] if applicable. Ensure the component supports [FEATURES] like lazy loading, inline validation, or polling. Example structure: ```php
#[AsLiveComponent('component_name')]
class ComponentNameComponent
{
#[LiveProp(writable: true)]
public string $searchTerm = '';
#[LiveProp]
public array $items = [];
#[LiveAction]
public function updateItems() { ... }
}
```Here’s a complete Symfony UX LiveComponent for a real-time search feature with lazy loading and inline validation:
```php
// src/Components/SearchComponent.php
namespace App\Components;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;
#[AsLiveComponent('search')]
class SearchComponent
{
use DefaultActionTrait;
#[LiveProp(writable: true)]
public string $query = '';
#[LiveProp]
public array $results = [];
#[LiveProp]
public bool $isLoading = false;
#[LiveAction]
public function search()
{
$this->isLoading = true;
// Simulate API delay
sleep(1);
// Mock search logic (replace with real service)
$this->results = array_filter(
['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'],
fn(string $item) => stripos($item, $this->query) !== false
);
$this->isLoading = false;
}
}
```
```twig
{# templates/components/search.html.twig #}
<div {{ init_live_component(this) }}>
<input
type="text"
data-model="query"
@input.debounce.300ms="search"
placeholder="Search..."
value="{{ query }}"
/>
{% if isLoading %}
<div class="spinner"></div>
{% endif %}
<ul>
{% for item in results %}
<li>{{ item }}</li>
{% else %}
<li>No results found</li>
{% endfor %}
</ul>
</div>
```
Key features implemented:
1. **Reactive Search**: The component re-renders after each keystroke (debounced by 300ms) without full page reloads.
2. **Loading State**: Shows a spinner during the simulated API delay.
3. **Zero JavaScript**: All reactivity is handled by Symfony UX LiveComponent.
4. **Lazy Loading**: Results are fetched only when the query changes.
Usage in a controller:
```php
#[Route('/search', name: 'app_search')]
public function search(SearchComponent $component): Response
{
return $this->render('search/index.html.twig', [
'component' => $component,
]);
}
```
In your Twig template:
```twig
{{ component('search') }}
```skills-collection
Take a free 3-minute scan and get personalized AI skill recommendations.
Take free scan