symfony/ux-live-component
Build interactive UIs in Symfony with Live Components: stateful server-driven components that update via Ajax without writing much JavaScript. Integrates with Twig, Stimulus and Symfony UX for reactive forms, lists, and real-time interactions.
Installation Add the package via Composer:
composer require symfony/ux-live-component
Ensure your Symfony app is on v6.3+ (or v7.0+ for latest features).
Enable Live Components
In config/packages/ux_live_component.yaml, enable the bundle:
framework:
ux_live_component:
enabled: true
First Live Component
Create a basic component in src/Components/ExampleComponent.php:
namespace App\Component;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\DefaultActionTrait;
#[AsLiveComponent('example')]
class ExampleComponent
{
use DefaultActionTrait;
public string $name = 'World';
public function greet(): string
{
return "Hello, {$this->name}!";
}
}
Render in Twig
Use the component in a Twig template (templates/example.html.twig):
{{ component('example') }}
Or inline:
{{ component('example', { name: 'Alice' }) }}
Trigger Updates Use JavaScript to update the component:
// In your asset (e.g., app.js)
import { startStimulusApp } from '@symfony/stimulus-bridge';
startStimulusApp();
// Or manually trigger updates
document.querySelector('[data-controller="live"]').action('update');
#[LiveProp] to auto-trigger updates:
#[LiveProp]
public int $count = 0;
#[LiveAction] to handle user interactions:
#[LiveAction]
public function increment(): void
{
$this->count++;
}
Call from Twig:
<button {{ action('increment') }}>+</button>
#[AsLiveComponent('parent')]
class ParentComponent
{
public ChildComponent $child;
}
Twig:
{{ component('child') }}
#[LiveAction] with use Symfony\UX\LiveComponent\Attribute\LiveAction for async operations:
#[LiveAction]
public function fetchData(): void
{
$data = $this->fetchFromApi(); // Simulate API call
$this->data = $data;
}
Twig:
<button {{ action('fetchData') }}>Load Data</button>
#[LiveComponent] with Symfony Forms:
use Symfony\Component\Form\Extension\Core\Type\TextType;
#[AsLiveComponent('form_example')]
class FormComponent
{
public function buildForm(): void
{
$this->form = $this->createFormBuilder()
->add('name', TextType::class)
->getForm();
}
}
Twig:
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
// assets/controllers/example_controller.js
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
connect() {
console.log('Component connected!');
}
}
Twig:
<div {{ stimulus('example') }}>
{{ component('example') }}
</div>
#[LiveComponent] for SSR + client-side hydration:
#[AsLiveComponent('hybrid')]
class HybridComponent
{
public function renderInEnvironment(Environment $environment): string
{
return match ($environment) {
Environment::SERVER => 'Server-rendered HTML',
Environment::CLIENT => 'Client-side HTML',
};
}
}
Circular Dependencies
A uses B, B uses A). Use dependency injection or lazy-loading.State Not Updating
#[LiveProp] or are public. Private properties won’t trigger updates.Form Submission Issues
enctype="multipart/form-data" for file uploads.{{ form_errors(form) }} to debug form validation errors.Stimulus Not Working
@symfony/stimulus-bridge is installed and imported in your JS entrypoint.data-controller attributes are correctly set in Twig.Performance with Large Data
#[LiveProp(write: false)] for read-only properties to reduce payload size.#[LiveAction] methods.#[LiveAction]
public function debug(): void
{
error_log(print_r($this->getState(), true));
}
F12) → Network tab to inspect live component updates (look for /_live_component endpoints).# config/packages/dev/framework.yaml
framework:
http_cache:
enabled: false
Custom Hydration
Override hydrate() to initialize state from client-side data:
public function hydrate(array $data): void
{
$this->name = $data['name'] ?? 'Guest';
}
WebSocket Integration
Use #[LiveAction] with Symfony Messenger or Mercure to push updates:
#[LiveAction]
public function subscribe(): void
{
$this->dispatch(new SubscribeToUpdatesEvent());
}
Type Safety Use PHP 8.2+ attributes for stricter typing:
#[LiveProp(type: 'int')]
public int $count = 0;
Testing
Test components with LiveComponentTestCase:
use Symfony\UX\LiveComponent\Test\LiveComponentTestCase;
class ExampleComponentTest extends LiveComponentTestCase
{
public function testGreet(): void
{
$component = $this->createComponent(ExampleComponent::class);
$this->assertSelectorTextContains('h1', 'Hello, World!');
}
}
config/packages/ux_live_component.yaml:
framework:
ux_live_component:
endpoint: /api/live
GET/HEAD requests. For POST, ensure CSRF tokens are included in forms.Custom Serializer
Extend LiveComponentSerializer to modify payloads:
use Symfony\UX\LiveComponent\Serializer\LiveComponentSerializer;
class CustomSerializer extends LiveComponentSerializer
{
protected function serializeState(array $state): array
{
// Custom logic
return parent::serializeState($state);
}
}
Register in services:
services:
Symfony\UX\LiveComponent\Serializer\LiveComponentSerializer:
class: App\Serializer\CustomSerializer
Event Listeners Listen to component lifecycle events:
use Symfony\UX\LiveComponent\Event\LiveComponentEvent;
class MyListener
{
public function onComponentRendered(LiveComponentEvent $event): void
{
$event->getComponent()->log('Rendered!');
}
}
Register in services.yaml:
services:
App\Listener\MyListener:
tags:
- { name: 'kernel.event_listener', event: 'ux.live_component.rendered' }
How can I help you explore Laravel packages today?