Skip to content

Effects & results

handle() returns an ActionResult: a success/failure flag, optional data, and a list of effects the client runs in order once the action responds.

Build a result from one of two constructors, optionally passing data:

ActionResult::success(['id' => $product->id]);
ActionResult::failure(['reason' => 'locked']);

Then chain effects — each returns a new result, so they read as a pipeline:

return ActionResult::success()
->toast('Archived.')
->reloadComponent('app.products');
EffectWhat it does
->toast($message, $variant)Shows a toast. Variant defaults to success.
->callout($callout)Shows a persistent in-flow banner in the layout’s callout slot.
->reloadComponent($id)Re-fetches a single component (e.g. the table the action changed).
->reloadPage()Reloads the current page’s props.
->to($url) / ->toRoute($name, $params) / ->back()Navigates to a URL, a named route, or the previous page.
->download($url)Triggers a file download.
->openModal($id) / ->closeModal($id)Opens or closes a modal by id (closeModal() closes the current modal).
->resetForm($id)Resets a form to its initial values (resetForm() resets the current form).
->localeChange($locale)Persists the frontend locale and dispatches lattice:locale-change.
->toggleSidebar($target)Toggles the layout sidebar (optionally a named target).
return ActionResult::success()
->toast('Report ready.', Variant::Success)
->download(route('reports.download', $report));

->toast() accepts a message and an optional Variant (Success, Error, Warning, Info). The variant can come first or second — both ->toast('Saved.') and ->toast(Variant::Error, 'Could not save.') read naturally. Pass a ToastMessage instead to set a lifetime, control dismissal, or attach a link or action — see Toasts.

A callout is a persistent in-flow banner — appropriate for warnings, trials, subscription notices, or any message that should stay visible until the user acts. Unlike a toast, a callout is not transient: it appears in the layout where the Callouts slot is placed and stays until the user dismisses it. There is no duration or auto-dismiss, and navigating between pages within the same layout does not clear it.

Build a Callout value object and pass it to ->callout():

use Lattice\Lattice\Core\Enums\Variant;
use Lattice\Lattice\Core\Values\Callout;
return ActionResult::success()
->callout(
Callout::make(Variant::Warning, 'Your trial ends in 3 days.')
->title('Trial ending')
->link('Upgrade', '/billing')
);

The Callout builder options:

MethodEffect
->title($string)Optional heading above the message.
->dismissible(bool)Show or hide the close button (default: dismissible).
->link($label, $href, $method?)Render a link in the callout ($method defaults to HttpMethod::Get).
->action($component)Render an action instead of a link.

Callouts are always persistent — there is no duration or auto-dismiss.

Placing the slot in your layout

A callout only renders where the Callouts layout slot is placed. Add Callouts::make() to the layout’s schema(), typically between the header bar and Outlet::make():

use Lattice\Lattice\Layouts\Components\Callouts;
use Lattice\Lattice\Layouts\Components\Outlet;
public function schema(): array
{
return [
$this->headerBar(),
Callouts::make(),
Outlet::make(),
];
}

Callout vs. toast

CalloutToast
PlacementIn-flow, where the layout slot isOverlay, anchored bottom center
PersistenceAlways persistentAuto-dismisses after a duration
ScopeRequires the layout slotGlobal — rendered by the <Toaster>

Use a callout when the message warrants visible, persistent attention; use a toast for transient confirmations. See Toasts for toast details.

After an action mutates data, refresh just the affected component rather than the whole page. ->reloadComponent() takes the component id — a table’s #[AsTable] id, for example — so only that component re-fetches:

return ActionResult::success()->reloadComponent('app.products');

Effects::flash() sends any effect(s) into the session and delivers them with the next Inertia response — no ActionResult needed. Use it from a controller, before returning a redirect, in an event listener, or in middleware — anywhere outside an action:

use Lattice\Lattice\Core\Values\Callout;
use Lattice\Lattice\Core\Enums\Variant;
use Lattice\Lattice\Facades\Effects;
use Lattice\Lattice\Effects\Effect;
// Flash a callout after a controller redirect
Effects::flash(
Effect::callout(
Callout::make(Variant::Info, 'Your export is being processed.')
->title('Export queued')
)
);
return redirect('/exports');

Pass multiple effects to flash them all at once:

Effects::flash(
Effect::toast(Variant::Success, 'Settings saved.'),
Effect::callout(Callout::make(Variant::Warning, 'Some changes require a page reload.')),
);

The flashed effects are stored in the latticeEffects session bag, drained on the next request, and run through the normal client-side effect pipeline in order.

The result serializes to { ok, data, effects }. Each effect carries a type discriminant (toast, callout, reloadComponent, reloadPage, redirect, download, openModal, closeModal, resetForm, localeChange, toggleSidebar) and its payload; the client dispatches them in order. Each type is declared once on its PHP class via #[AsEffect('…')] and generated into the discriminated Effect union, so the PHP helpers and the client dispatcher stay in lockstep.

The effect system is extensible — define your own effect type and a client handler for it, the same way components and columns extend the renderer.

On the server, an effect is a readonly value object marked with #[AsEffect], where the constructor arguments are the wire payload:

use Lattice\Lattice\Effects\Attributes\AsEffect;
use Lattice\Lattice\Effects\Effect;
#[AsEffect('confetti')]
final readonly class ConfettiEffect extends Effect
{
public function __construct(public int $pieces = 100) {}
}

Return it from handle() with ->effect(), alongside any built-ins:

return ActionResult::success()
->toast('Onboarding complete!')
->effect(new ConfettiEffect(pieces: 250));

On the client, author a handler with effectHandler("confetti", …) and register it through your registry plugin’s effects map. The handler receives the typed payload:

import { effectHandler } from "@lattice-php/lattice";
const confetti = effectHandler("confetti", (effect) => {
launchConfetti(effect.pieces);
});

Unknown effect types are skipped with a console warning, so a server effect with no registered handler fails soft rather than breaking the page.