Documentation menu

Actions

Actions are server-side event handlers that the browser can trigger. When a user clicks a button or submits a form, Datastar POSTs to the action's URL and your PHP callback runs — signals update, views re-render, and patches stream back via SSE.

Creating an action

$increment = $c->action(function (Context $c) use ($count): void {
    $count->setValue($count->int() + 1);
    $c->sync();
}, 'increment');

The first argument is the callback; the second is an optional name used in the URL. The name defaults to an auto-generated ID if omitted, but a name makes debugging easier.

Action URL

Get the URL to pass into your Twig template:

$increment->url();  // '/_action/increment'

In Twig, bind it to a Datastar event attribute:

<button data-on:click="@post('{{ increment_url }}')">>+1</button>

Reading POST data

POST body and query params are accessible via $_POST and $_GET inside the callback:

$vote = $c->action(function (Context $c) use ($app, $votes): void {
    $option = $_POST['option'] ?? ($_GET['option'] ?? null);
    if (!in_array($option, ['a', 'b', 'c'], true)) {
        return; // ignore unknown values
    }
    $app->setGlobalState('vote_' . $option, ($app->globalState('vote_' . $option) ?? 0) + 1);
    $votes->setValue(['a' => $app->globalState('vote_a'), 'b' => $app->globalState('vote_b')]);
    $app->broadcast(Scope::routeScope('/'));
}, 'vote');
<!-- Pass option as query param -->
<button data-on:click="@post('{{ vote_url }}?option=a')">Vote A</button>

Syncing after an action

Actions don't auto-sync. You must call $c->sync() (view + signals) or $c->syncSignals() (signals only, faster) to push changes to the browser:

$c->action(function (Context $c) use ($label): void {
    $label->setValue('Saved!');
    $c->syncSignals(); // only the signal changed — skip view re-render
}, 'save');

For ROUTE-scoped signals, use $app->broadcast() instead so all clients on the route receive the update. See Broadcasting for details.

Multiple actions per context

A context can have as many actions as needed. Each gets its own URL:

$inc   = $c->action(fn(Context $c) use ($count) => $count->setValue($count->int() + 1) & $c->sync(), 'inc');
$dec   = $c->action(fn(Context $c) use ($count) => $count->setValue($count->int() - 1) & $c->sync(), 'dec');
$reset = $c->action(fn(Context $c) use ($count) => $count->setValue(0) & $c->sync(), 'reset');

Cleanup actions

For actions that should only run once, or that need cleanup when the user leaves, combine with lifecycle hooks:

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

Next steps

  • Signals — the state that actions modify
  • Broadcasting — pushing updates to multiple clients
  • Views — how sync() triggers a view re-render
  • Lifecycle — cleanup hooks and timers