Actions
An action runs on the server in response to a click and returns effects the client dispatches: a toast, a redirect, a component or page refresh, opening a modal. An action can both change data and drive the UI that follows.
How an action runs
Section titled “How an action runs”A click posts the action’s signed reference to its endpoint. The server verifies the reference (and the
trusted context baked into it), authorizes, runs handle(), and returns an ActionResult whose
effects the client then dispatches:
Defining an action
Section titled “Defining an action”Extend ActionDefinition and implement two methods: definition() describes the trigger (label,
icon, variant, confirmation), and handle() runs the work and returns an ActionResult. The
#[AsAction] attribute gives the action a stable id so it can be discovered and addressed by its
endpoint.
use App\Models\Product;use Illuminate\Http\Request;use Lattice\Actions\ActionDefinition;use Lattice\Actions\ActionResult;use Lattice\Actions\Components\Action;use Lattice\Core\Attributes\AsAction;use Lattice\Core\Concerns\ResolvesContextModels;use Lattice\Ui\Enums\Emphasis;use Lattice\Ui\Enums\Variant;
#[AsAction('app.products.archive')]class ArchiveProductAction extends ActionDefinition{ use ResolvesContextModels;
public function definition(Action $action): Action { return $action ->label('Archive') ->variant(Variant::Danger) ->confirm('Archive product?', 'This hides it from the catalogue.'); }
public function handle(): ActionResult { $product = $this->contextModel('product_id', Product::class); $product->update(['status' => 'archived']);
return ActionResult::success() ->toast('Product archived.', Variant::Success) ->reloadComponent('app.products'); }}contextModel() reads the sealed context and resolves it into the model through its own
route binding — it aborts with a 404 when the key is missing or nothing
matches. That two-argument form comes from the opt-in ResolvesContextModels trait rather than
context() itself, which stays untyped on every definition.
When a resolver for the key is registered once via
Lattice::context('product', Product::class), the same call shrinks to a one-argument
contextModel('product') — memoized for the request, and usable directly on Definition without the
trait.
Placing an action
Section titled “Placing an action”Reference an action anywhere a component is accepted with Action::use(). The most common spot is a
table’s row actions, where ->context() scopes it to the record:
Action::use(ArchiveProductAction::class) ->context(['product_id' => $row['id']]);->context() carries data from the page to the action; handle() reads it back server-side, typically
through a typed accessor like contextModel() (see Defining an action). The
context is signed into the action’s reference, so it can’t be tampered with on the way back.
The value doesn’t have to be the scalar id — pass the model directly when you already have it, and Lattice normalizes it to the same scalar before the action gates or seals its context:
Action::use(ArchiveProductAction::class)->context(['product' => $product]);This only works for a key with a registered resolver; see Context.
Group related actions behind a single trigger with ActionGroup:
use Lattice\Actions\Components\ActionGroup;
ActionGroup::make('row-actions')->actions([ Action::use(EditProductAction::class)->context(['product_id' => $row['id']]), Action::use(ArchiveProductAction::class)->context(['product_id' => $row['id']]),]);Render the same group inline when the actions should stay visible:
use Lattice\Ui\Enums\Orientation;
ActionGroup::make('locale-switcher') ->label('Language') ->inline(Orientation::Horizontal) ->actions([ Action::use(SetLocaleAction::class)->context(['locale' => 'en']), Action::use(SetLocaleAction::class)->context(['locale' => 'de']), ]);The result
Section titled “The result”handle() returns an ActionResult. Start from ActionResult::success(),
optionally attaching data, then chain effects:
return ActionResult::success(['id' => $product->id]) ->toast('Saved.') ->reloadComponent('app.products');Authorization
Section titled “Authorization”Override authorize() to gate an action. It receives the request — the signed context is available
through $this->context() — and returns a boolean; a denied action never reaches handle().
public function authorize(Request $request): bool{ return $this->contextModel('product_id', Product::class)->status !== 'archived';}An action’s authorize() also runs at render time — when it’s hidden from the page rather than
rejected outright, a strict accessor’s 404 takes the whole page down with it. See
Context for the OrNull variants to reach for there instead.
Calling an action from a custom component
Section titled “Calling an action from a custom component”A custom component can carry an action as a prop and run it with
its own payload instead of rendering the built-in trigger. Declare a nullable Action prop on the
PHP component and fill it with Action::use():
use Lattice\Actions\Components\Action;use Lattice\Core\Attributes\AsComponent;use Lattice\Ui\Components\Component;
#[AsComponent('color-picker')]final class ColorPicker extends Component{ public ?Action $saveAction = null;
public function saveAction(string $action, array $context = []): static { $this->saveAction = Action::use($action, $context);
return $this; }}On the client, callAction does the whole round trip: it posts a JSON payload to the action’s
endpoint with its signed ref, dispatches the returned effects, and resolves with the response status
and the data attached via ActionResult::success([...]). The useCallAction() hook binds it to the
current effect dispatcher:
import type { RendererComponent } from "@lattice-php/core/types";import { useCallAction } from "@lattice-php/lattice/runtime";
const ColorPicker: RendererComponent<"color-picker"> = ({ node }) => { const callAction = useCallAction();
const save = async (color: string) => { if (!node.props.saveAction) { return; }
const { ok, data } = await callAction(node.props.saveAction, { color });
if (ok) { applyColor(String(data.color ?? color)); } };
// ...};An action the server hid or denied serializes without an endpoint; callAction treats it as an ok
no-op, so optimistic UI keyed on the result stays put. A rejected action (non-2xx) still dispatches
its effects — the error toast shows — and resolves ok: false with the response data. Outside a
hook context, use callAction(action, payload, dispatch) from @lattice-php/action with the
dispatcher useEffectDispatcher() returns.
Next steps
Section titled “Next steps”- Effects & results — every effect an action can return.
- Confirmation & forms — confirmation modals and collecting input before running.
- Bulk actions — acting on a table selection.