Symfony Live ComponentsとUX 3.0:JavaScriptなしでリアクティブアプリを構築する方法(2026年版)

Symfony Live ComponentsとUX 3.0を使用して、JavaScriptフレームワーク不要でリアクティブなUIを構築する方法を解説します。LiveProp、LiveAction、フォーム処理、遅延読み込み、URLバインディングを網羅したチュートリアルです。

Symfony Live ComponentsとUX 3.0によるPHPとTwigでのリアクティブインターフェース

Symfony Live Componentsは、PHPとTwigだけでリアクティブかつリアルタイムなユーザーインターフェースを実現する仕組みです。2026年4月にリリースされたSymfony UX 3.0では、2.x系の非推奨機能がすべて削除され、最低要件がPHP 8.4およびSymfony 7.4に引き上げられました。さらに、4つの廃止パッケージが除外され、これまでで最も軽量なUXツールキットとなっています。

Live Componentsとは?

Live Componentsは、あらゆるTwigコンポーネントをステートフルでインタラクティブな要素に変換します。ユーザー操作に応じてサーバーサイドで再レンダリングが行われ、JavaScriptを一切記述する必要がありません。データバインディング、アクション、バリデーション、フォーム処理はすべてPHP内で完結します。

Symfony UX 3.0とLive Componentsのセットアップ

インストールにはSymfony 7.4以上、PHP 8.4以上、そしてフロントエンドアセット用にAssetMapperまたはWebpack Encoreが必要です。

bash
# terminal
composer require symfony/ux-live-component

# With AssetMapper (recommended for Symfony 7.4+)
php bin/console importmap:require @symfony/ux-live-component

# Verify installation
php bin/console debug:twig --filter=component

AssetMapperは、Node.js、Webpack、その他のビルドステップを一切使用せずにJavaScriptを配信します。Live Componentsを駆動するStimulusコントローラーは自動的にロードされます。

LivePropを使ったリアクティブ検索コンポーネントの作成

ユーザーが入力するたびに結果をフィルタリングする検索バーを、JavaScriptゼロで実装できます。#[LiveProp]属性はプロパティをステートフルとしてマークし、暗号化されたURLセーフトークンを通じて再レンダリング間で値を保持します。

php
<?php
// src/Twig/Components/ProductSearch.php

namespace App\Twig\Components;

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

#[AsLiveComponent]
class ProductSearch
{
    use DefaultActionTrait;

    // writable: true allows the frontend to modify this property
    // url: true syncs the value with ?query= in the address bar
    #[LiveProp(writable: true, url: true)]
    public string $query = '';

    #[LiveProp(writable: true)]
    public string $category = 'all';

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

    public function getProducts(): array
    {
        if (strlen($this->query) < 2) {
            return [];
        }

        return $this->productRepository->search(
            query: $this->query,
            category: $this->category === 'all' ? null : $this->category,
            limit: 20,
        );
    }
}

対応するTwigテンプレートでは、data-modelを使って入力をプロパティにバインドします。

twig
{# templates/components/ProductSearch.html.twig #}
<div {{ attributes }}>
    <input
        type="search"
        data-model="debounce(300)|query"
        placeholder="Search products..."
        class="form-input w-full"
    />

    <select data-model="category" class="form-select mt-2">
        <option value="all">All categories</option>
        <option value="electronics">Electronics</option>
        <option value="books">Books</option>
    </select>

    <div class="mt-4">
        {% for product in this.products %}
            <div key="{{ product.id }}" class="border-b py-2">
                <h3>{{ product.name }}</h3>
                <span>{{ product.price|format_currency('EUR') }}</span>
            </div>
        {% else %}
            {% if query|length >= 2 %}
                <p>No results for "{{ query }}".</p>
            {% endif %}
        {% endfor %}
    </div>
</div>

debounce(300)修飾子は、最後のキーストロークから300ミリ秒待ってからリクエストを送信します。リストアイテムのkey属性は効率的なDOMモーフィングを可能にし、変更された要素のみが更新されます。

LivePropによるURLバインディング

LivePropでurl: trueを設定すると、プロパティの値がクエリパラメータとしてブラウザURLに同期されます。たった1つの属性で、ブックマーク可能で共有可能な検索結果が実現します。

LiveActionによるユーザーアクションの処理

データバインディングに加えて、#[LiveAction]はPHPメソッドをフロントエンドから呼び出し可能なアクションとして公開します。引数は#[LiveArg]を通じて渡されます。

php
<?php
// src/Twig/Components/ShoppingCart.php

namespace App\Twig\Components;

use App\Entity\CartItem;
use App\Service\CartService;
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\ComponentToolsTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;

#[AsLiveComponent]
class ShoppingCart
{
    use DefaultActionTrait;
    use ComponentToolsTrait;

    #[LiveProp]
    public array $items = [];

    public function __construct(
        private CartService $cartService,
    ) {
    }

    #[LiveAction]
    public function addItem(#[LiveArg] int $productId, #[LiveArg] int $quantity = 1): void
    {
        $this->cartService->add($productId, $quantity);
        $this->items = $this->cartService->getItems();

        // Notify other components on the page
        $this->emit('cart:updated', [
            'count' => count($this->items),
        ]);
    }

    #[LiveAction]
    public function removeItem(#[LiveArg] int $itemId): void
    {
        $this->cartService->remove($itemId);
        $this->items = $this->cartService->getItems();
        $this->emit('cart:updated', ['count' => count($this->items)]);
    }

    public function getTotal(): float
    {
        return array_sum(array_map(
            fn(CartItem $item) => $item->getPrice() * $item->getQuantity(),
            $this->items,
        ));
    }
}

テンプレートでは、live_action()ヘルパーまたはdata-action属性を使用してアクションをトリガーします。

twig
{# templates/components/ShoppingCart.html.twig #}
<div {{ attributes }}>
    <h2>Cart ({{ items|length }})</h2>

    {% for item in items %}
        <div key="{{ item.id }}" class="flex justify-between py-2">
            <span>{{ item.name }} x{{ item.quantity }}</span>
            <span>{{ (item.price * item.quantity)|format_currency('EUR') }}</span>
            <button {{ live_action('removeItem', { itemId: item.id }) }}
                    class="text-red-600">
                Remove
            </button>
        </div>
    {% endfor %}

    <div class="border-t pt-2 font-bold">
        Total: {{ this.total|format_currency('EUR') }}
    </div>
</div>

emit()メソッドは、ページ上の他のLive Componentsにイベントをブロードキャストします。例えば、カートバッジコンポーネントは#[LiveListener('cart:updated')]でリッスンし、ページ全体をリロードすることなくカウントを更新できます。

Symfonyの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

JavaScriptなしのリアルタイムフォームバリデーション

Live ComponentsはSymfony Formsと直接統合されます。ComponentWithFormTraitは、フォームの状態、バリデーションエラー、送信をコンポーネントのライフサイクルに接続します。

php
<?php
// src/Twig/Components/RegistrationForm.php

namespace App\Twig\Components;

use App\Entity\User;
use App\Form\RegistrationType;
use Doctrine\ORM\EntityManagerInterface;
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]
class RegistrationForm extends AbstractController
{
    use DefaultActionTrait;
    use ComponentWithFormTrait;

    #[LiveProp]
    public ?User $initialFormData = null;

    protected function instantiateForm(): FormInterface
    {
        return $this->createForm(RegistrationType::class, $this->initialFormData ?? new User());
    }

    #[LiveAction]
    public function save(EntityManagerInterface $em): mixed
    {
        // Submits + validates — re-renders with errors if invalid
        $this->submitForm();
        $user = $this->getForm()->getData();

        $em->persist($user);
        $em->flush();

        $this->addFlash('success', 'Account created.');

        return $this->redirectToRoute('app_login');
    }
}
twig
{# templates/components/RegistrationForm.html.twig #}
<div {{ attributes }}>
    {{ form_start(form, {
        attr: {
            'data-action': 'live#action:prevent',
            'data-live-action-param': 'save'
        }
    }) }}
        {{ form_row(form.email) }}
        {{ form_row(form.plainPassword, { label: 'Password' }) }}
        {{ form_row(form.fullName) }}

        <button type="submit"
                class="btn-primary w-full"
                data-loading="addAttribute(disabled) addClass(opacity-50)">
            <span data-loading="hide">Create Account</span>
            <span data-loading="show" class="animate-spin">Loading...</span>
        </button>
    {{ form_end(form) }}
</div>

バリデーションエラーは、ユーザーがフィールド間を移動する際にインラインで表示されます。data-modelon(change)修飾子がblur時に再レンダリングをトリガーし、Symfony Validatorの制約メッセージが即座に表示されます。

パフォーマンスのための遅延読み込みとレイジーローディング

ダッシュボード、分析チャート、長いリストなどの重いコンポーネントは、遅延レンダリングの恩恵を受けます。初期ページロードをブロックする代わりに、コンポーネントはプレースホルダーをレンダリングし、ページの準備ができた後にAJAXでコンテンツを取得します。

php
<?php
// src/Twig/Components/AnalyticsDashboard.php

namespace App\Twig\Components;

use App\Service\AnalyticsService;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;

#[AsLiveComponent]
class AnalyticsDashboard
{
    use DefaultActionTrait;

    #[LiveProp]
    public string $period = '30d';

    public function __construct(
        private AnalyticsService $analytics,
    ) {
    }

    public function getMetrics(): array
    {
        // Expensive query — runs only after page load
        return $this->analytics->getMetrics($this->period);
    }
}
twig
{# Parent page: load the dashboard after the page renders #}
<twig:AnalyticsDashboard loading="defer" period="30d" />

{# Or load when scrolled into view #}
<twig:AnalyticsDashboard loading="lazy" period="30d" />

コンポーネントテンプレート内のプレースホルダーマクロは、実際のコンテンツが読み込まれるまでユーザーに表示される内容を定義します。

twig
{# templates/components/AnalyticsDashboard.html.twig #}
<div {{ attributes }}>
    <h2>Analytics — {{ period }}</h2>
    {% for metric in this.metrics %}
        <div class="stat-card">
            <span class="stat-label">{{ metric.label }}</span>
            <span class="stat-value">{{ metric.value|number_format }}</span>
        </div>
    {% endfor %}
</div>

{% macro placeholder(props) %}
    <div class="animate-pulse space-y-4">
        <div class="h-6 bg-gray-200 rounded w-1/3"></div>
        <div class="h-20 bg-gray-200 rounded"></div>
        <div class="h-20 bg-gray-200 rounded"></div>
    </div>
{% endmacro %}

loading="defer"モードはページロード直後にAJAXリクエストを発行します。loading="lazy"モードはIntersectionObserverを使用し、コンポーネントがビューポートにスクロールされたときにのみリクエストが発行されます。どちらのアプローチも初期ページレスポンスを高速に保ちます。

UX 3.0でCSRFが廃止

Symfony UX 3.0では、Live ComponentsのCSRFトークンが同一オリジン/CORSプロテクションに置き換えられました。#[AsLiveComponent]csrf引数はもう存在しません。サーバーが同一オリジンポリシーを適用していることを確認してください。

イベントによるコンポーネント間通信

Live Componentsはイベントシステムを通じて通信します。子コンポーネントがイベントを発行し、親(または兄弟)コンポーネントがリッスンして反応します。

php
<?php
// src/Twig/Components/CartBadge.php

namespace App\Twig\Components;

use App\Service\CartService;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveListener;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;

#[AsLiveComponent]
class CartBadge
{
    use DefaultActionTrait;

    #[LiveProp]
    public int $count = 0;

    public function __construct(CartService $cartService)
    {
        $this->count = $cartService->getItemCount();
    }

    // Automatically re-renders when ShoppingCart emits 'cart:updated'
    #[LiveListener('cart:updated')]
    public function onCartUpdated(#[LiveArg] int $count): void
    {
        $this->count = $count;
    }
}

このパターンにより、コンポーネント間の結合が疎になります。ShoppingCartコンポーネントはCartBadgeを直接参照せず、イベントバスが接続を処理します。

Symfony UX 3.0の主な変更点

UX 3.0はクリーンアップリリースです。2.x系で非推奨警告なしに動作しているアプリケーションは、最小限の手間でアップグレードできます。

| 変更点 | 以前(2.x) | 以後(3.0) | |--------|-------------|-------------| | CSRFプロテクション | #[AsLiveComponent]csrf: true | 同一オリジン/CORS(自動) | | Twig CVA関数 | cva() | twig/html-extra 3.12+のhtml_cva() | | コンポーネントデフォルト設定 | 任意 | twig_component.defaultsが必須 | | 削除されたパッケージ | Swup, LazyImage, Typed, TogglePassword | ネイティブAPIまたはUX Toolkit | | PHP要件 | 8.1以上 | 8.4以上 | | Symfony要件 | 6.4以上 | 7.4以上 |

2.xサイクル中に導入されたUX Toolkitは、Shadcn UIまたはFlowbite 4.0でスタイリングされたプリビルドUIコンポーネント(Button、Dialog、Card、Table、Pagination)を提供し、削除されたパッケージの穴を埋めます。

Symfonyの面接対策を準備している方にとって、Live Componentsとイベントシステムの理解はシニアレベルのポジションでますます重要になっています。Symfony面接対策ガイドでは、この実践的な知識と相性の良い基礎的なトピックを取り上げています。

まとめ

  • Live Componentsは、CRUDが中心となるSymfonyアプリケーションのほとんどにおいて、JavaScriptフレームワークの必要性を排除します。データバインディング、アクション、バリデーション、フォーム処理はすべてPHPとTwigで完結します
  • #[LiveProp(writable: true, url: true)]は、たった1つの属性でブックマーク可能かつ共有可能なステートフルインターフェースを作成します
  • 遅延読み込みとレイジーローディング(loading="defer" / loading="lazy")は、重いコンポーネントが非同期でレンダリングされる間、初期ページロードを高速に保ちます
  • UX 3.0はCSRFトークンを廃止し、同一オリジン/CORSプロテクションを採用しました。可動部品が少ないシンプルなセキュリティです
  • イベントシステム(emit / #[LiveListener])は、グローバルな状態管理なしでコンポーネント間の疎結合な通信を実現します
  • API連携には、バックエンド中心のアーキテクチャ向けにSymfony 7でのAPI Platformを補完的に活用できます

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

タグ

#symfony
#live components
#symfony ux
#php
#twig
#reactive ui

共有

関連記事