symfony/ux-live-component
Build interactive UIs in Symfony with Live Components: stateful Twig components that update via Ajax without writing custom JavaScript. Handle actions, validation, and form binding, with predictable server-side rendering and smooth partial updates.
Installation:
composer require symfony/ux-live-component
Ensure symfony/ux-twig-component is also installed (dependency).
Enable Bundle:
Add to config/bundles.php:
return [
// ...
Symfony\UX\LiveComponent\LiveComponentBundle::class => ['all' => true],
];
Basic Component:
Create a controller with #[AsLiveComponent]:
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
#[AsLiveComponent]
class CounterComponent
{
#[LiveProp]
public int $count = 0;
public function increment(): void
{
$this->count++;
}
}
Template (templates/counter.html.twig):
<div>
Count: {{ count }}
<button data-action="click->increment">+</button>
</div>
Register Route:
# config/routes.yaml
_ux_live_component:
resource: '@LiveComponentBundle/config/routes.php'
prefix: /_components
Use in Parent Template:
{{ component('app_counter') }}
Replace a traditional form submission with real-time updates. For example, a search bar that updates results without page reloads.
Model Binding:
#[LiveProp] to bind form inputs to component properties.#[LiveProp(writable: ['name', 'email'])]
public User $user;
<input data-model="name" type="text">
<input data-model="email" type="email">
Actions:
save(), delete()).data-action:
<button data-action="click->save">Save</button>
Child Components:
{{ component('app_child_component', { prop: value }) }}
acceptUpdatesFromParent: true to propagate parent updates:
#[LiveProp(acceptUpdatesFromParent: true)]
public string $prop;
Forms:
ComponentWithFormTrait for seamless form handling:
use Symfony\UX\LiveComponent\Form\ComponentWithFormTrait;
#[AsLiveComponent]
class UserFormComponent
{
use ComponentWithFormTrait;
public function __construct(private EntityManagerInterface $em) {}
public function handleForm(FormInterface $form): void
{
if ($form->isSubmitted() && $form->isValid()) {
$this->em->persist($form->getData());
$this->em->flush();
}
}
}
{{ form_start(form) }}
{{ form_row(form.name) }}
{{ form_row(form.email) }}
<button type="submit">Submit</button>
{{ form_end(form) }}
Dynamic Templates:
FromMethod for dynamic template resolution:
#[AsLiveComponent(template: FromMethod('getTemplate'))]
class DynamicComponent
{
public function getTemplate(): string
{
return $this->user->isAdmin() ? 'admin_template.html.twig' : 'user_template.html.twig';
}
}
Validation:
Use ValidatableComponentTrait for form validation:
use Symfony\UX\LiveComponent\Form\ValidatableComponentTrait;
#[AsLiveComponent]
class ValidatedComponent
{
use ValidatableComponentTrait;
public function handleForm(FormInterface $form): void
{
if ($form->isSubmitted() && $this->validate($form)) {
// Handle valid form
}
}
}
Events: Emit custom events between components:
$this->emit('userUpdated', ['id' => $user->id]);
Listen in JavaScript:
component.on('userUpdated', (event) => {
console.log(event.detail);
});
Stimulus Integration: Combine with Stimulus for hybrid approaches:
{{ component('app_component', { controller: 'my-stimulus-controller' }) }}
Testing:
Use InteractsWithLiveComponents for component tests:
use Symfony\UX\LiveComponent\Test\InteractsWithLiveComponents;
class CounterComponentTest extends TestCase
{
use InteractsWithLiveComponents;
public function testIncrement(): void
{
$component = $this->createLiveComponent(CounterComponent::class);
$rendered = $component->render();
$this->assertSelectorTextContains('div', 'Count: 0');
$component->increment();
$this->assertSelectorTextContains('div', 'Count: 1');
}
}
CSRF and CORS:
fetchCredentials is configured for cross-origin requests:
#[AsLiveComponent(fetchCredentials: 'include')]
class MyComponent {}
config/packages/live_component.yaml:
live_component:
fetch_credentials: 'include'
Model Updates vs. Actions:
data-model) and actions (data-action) are not atomic. Use norender to avoid unintended re-renders:
<input data-model="norender|count">
Child Component Re-renders:
#[LiveProp(acceptUpdatesFromParent: true)]
public string $prop;
Form Handling:
data-action="live#update" from forms. Use data-model="on(change)|*" instead:
<form data-model="on(change)|*">
<input name="field">
</form>
Type Mismatches:
SVG and Dynamic Content:
data-live-id is set for dynamic content (e.g., SVGs) to maintain DOM structure during updates.Debugging:
debug:live-component command to inspect component state:
php bin/console debug:live-component
// In your Stimulus controller
this.component.on('response:error', (event) => {
console.error(event.detail);
});
Performance:
#[LiveProp(useSerializerForHydration: false)] for complex objects to avoid serialization overhead.min_length, max_length, etc., modifiers to reduce unnecessary updates:
<input data-model="min_length(3)|search">
Loading States:
data-loading for visual feedback:
<span data-loading="action(save)|show">Loading...</span>
Component Isolation:
{{ component('app_my_component', { id: 'unique-' ~ id }) }}
Mercure Integration:
$this->emit('userUpdated', ['id' => $user->id]);
// Subscribe via Mercure hub
Asset Mapping:
symfony/asset-mapper for dynamic asset paths:
#[LiveProp]
public string $imagePath = $this->assetMapper->getUrl('images/default.png');
TypeScript:
Component type for custom logic:
interface MyComponent extends Component {
increment(): void;
}
Fallbacks:
#[AsLiveComponent(template: FromMethod('getTemplate'))]
public function getTemplate(): string
{
return $this->user ? 'user_template.html.twig' : 'guest_template.html.twig';
}
Browser Events:
$this->dispatchBrowserEvent('custom:event', ['data' => 'value']);
document.addEventListener('
How can I help you explore Laravel packages today?