Documentation menu

Components

Components are reusable UI blocks backed by their own sub-context. Each component has isolated signals, actions, and a view — but shares the parent connection. They're the building block for composable real-time UIs.

Creating a component

Define a component as a callable that configures a Context:

$counterComponent = function (Context $c) use ($twig): void {
    $count = $c->signal(0, 'count');
    $inc   = $c->action(fn() => $count->setValue($count->int() + 1) & $c->sync(), 'inc');

    $c->view('components/counter.html.twig', [
        'count_id'  => $count->id(),
        'count_val' => $count->int(),
        'inc_url'   => $inc->url(),
    ]);
};

Mounting a component

Mount a component inside a page handler via $c->component(), giving it a namespace to prevent signal ID collisions. The returned callable renders the component's initial HTML:

$app->page('/', function (Context $c) use ($counterComponent): void {
    $counter = $c->component($counterComponent, 'counter');

    $c->view('pages/home.html.twig', [
        'counter' => $counter(), // initial HTML string
    ]);
});
{# pages/home.html.twig #}
<main>
    <h1>Welcome</h1>
    {{ counter | raw }}
</main>

Namespacing

The namespace prefixes all signal and action IDs in the component. This lets you mount the same component multiple times on one page without signal conflicts:

$counterA = $c->component($counterComponent, 'counter-a');
$counterB = $c->component($counterComponent, 'counter-b');

// counter-a has signal ID 'counter-a_count', action '/_action/counter-a_inc'
// counter-b has signal ID 'counter-b_count', action '/_action/counter-b_inc'

$c->view('page.html.twig', [
    'counterA' => $counterA(),
    'counterB' => $counterB(),
]);

Scoped components

Components can set their own scope, independent of the parent. This is how the website's shared counter and presence indicator work — they're ROUTE-scoped components inside a page:

$presenceComponent = function (Context $c) use ($app, $twig): void {
    $c->scope(Scope::ROUTE); // shared with everyone on this route

    $c->view(function () use ($app, $twig): string {
        $count = count($app->getClients());
        return $twig->render('components/presence.html.twig', [
            'count'  => $count,
            'person' => $count === 1 ? 'person' : 'people',
        ]);
    }, cacheUpdates: false);
};

Component lifecycle

Components follow the same lifecycle as contexts. They support onDisconnect, setInterval, and all other context methods:

$roomComponent = function (Context $c) use ($room, $app): void {
    $c->addScope('room:' . $room);
    // ... signals, view ...

    $c->onDisconnect(function (Context $c) use ($room, $app): void {
        Room::$members[$room]--;
        $app->broadcast('room:' . $room);
    });
};

Next steps

  • Scopes — share component state across users
  • Views — how component views update independently
  • Lifecycle — connect, disconnect, and timer hooks
  • API: component() — full method signature