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