Essentials
Lifecycle
A component lives in two places: the server creates it and answers its #[Server] calls, and the browser runs it in between. Hooks let you run code at each step of that life, on the side where the step happens.
On this page
A component's life
-
Created on the server
The page renders:
boot(), thenmount()(or the tag's values into its properties), then the view, thendehydrate(). Its state is signed and sent with the HTML. -
Live in the browser
The browser takes over the server's HTML and runs
mounted(). From then on its methods and view run there; a change from the view runsupdated*(). -
Called on the server
Each
#[Server]call rebuilds it from its signed state:boot(),hydrate(), the browser's changes checked and applied, the method,dehydrate(). The new state goes back, and the browser carries on. -
Gone
When it leaves the page (an
@if, a removed item, a visit),destroyed()runs in the browser.
A component created in the browser (a new item in a loop, with no mount()) skips the first step:
its properties come from its tag, and it starts at mounted().
The hooks
| Hook | Runs | When |
|---|---|---|
boot()
|
Server |
Every request: on creation and on each
#[Server]
call, before anything else
|
mount(...)
|
Server | Once, on creation. Takes the tag's or the route's values, and anything Laravel's container can give |
hydrate()
|
Server |
Each
#[Server]
call, once the state is restored, before the browser's changes are applied
|
dehydrate()
|
Server | Every request, last, before the state goes to the browser |
exception($e)
|
Server |
A
#[Server]
method threw: handled, unless the hook throws it again
|
fallback($e)
|
Server | The component failed, in production: what shows in its place (see Handling errors) |
mounted()
|
Browser, or
#[Server]
|
Once the component is live in the page |
updatingSearch($value)
|
Browser |
Before the view changes
$search
:
$this->search
still has the old value
|
updating($property, $value)
|
Browser | Before the view changes any property, with its path |
updatedSearch($value)
|
Browser, or
#[Server]
|
After the view changed
$search
|
updatedFilters($value, $key)
|
Browser, or
#[Server]
|
After the view changed a key under
$filters
: its value, and the key
|
updated($property, $value)
|
Browser, or
#[Server]
|
After the view changed any property, with its full path (
filters.owner
)
|
rendered()
|
Browser | After each render that changed the view |
destroyed()
|
Browser, or
#[Server]
|
When the component leaves the page |
On the server
<?php
namespace App\Nitro\Components;
use App\Models\Team;
use Nitro\Attributes\Locked;
use Nitro\Attributes\Server;
use Nitro\Component;
class TeamMembers extends Component
{
#[Locked]
public int $teamId;
public array $members = [];
/** Every request, first: on creation and on each #[Server] call, before anything else runs. */
public function boot(): void
{
abort_unless(auth()->check(), 401);
}
/** Once, when the component is created: its starting state. */
public function mount(Team $team): void
{
$this->teamId = $team->id;
$this->members = $team->members()->orderBy('name')->pluck('name')->all();
}
/** Each #[Server] call, once the state is restored, before the browser's changes are applied. */
public function hydrate(): void
{
logger()->debug('team members call', ['team' => $this->teamId]);
}
/** Every request, last, before the state goes to the browser. */
public function dehydrate(): void
{
$this->members = array_values(array_unique($this->members));
}
#[Server]
public function remove(string $name): void
{
Team::findOrFail($this->teamId)->members()->where('name', $name)->delete();
$this->members = array_values(array_diff($this->members, [$name]));
}
}
boot()is for what every request needs, such as a check that must pass on creation and on each call alike.mount()sets the starting state. It runs once: a#[Server]call doesn't run it again, because the state comes from the browser, signed.hydrate()sees the state as the server last sent it. The browser's changes are applied after it, then checked by#[Validate]; when a rule fails, the method doesn't run.dehydrate()runs once per request, whatever happened before it, so it is the place to tidy the state on its way out.
In the browser
These hooks are compiled to JavaScript like any browser method, so they run with no request:
use Nitro\Facades\Nitro;
public string $search = '';
public int $page = 1;
public array $filters = ['status' => 'open', 'owner' => null];
/** Once the component is live in the page. */
public function mounted(): void
{
$this->js('$refs.search.focus()');
}
/** After the view changes $search (nitro:model, $set): back to the first page. */
public function updatedSearch(string $value): void
{
$this->page = 1;
}
/** After the view changes a key under $filters: its new value, and which key. */
public function updatedFilters(mixed $value, ?string $key): void
{
if ($key === 'status') {
$this->page = 1;
}
}
/** After the view changes any property: its path ("filters.owner") and its new value. */
public function updated(string $property, mixed $value): void
{
Nitro::browser()->storage()->set('tickets.filters', $this->filters);
}
/** When the component leaves the page. */
public function destroyed(): void
{
Nitro::browser()->storage()->forget('tickets.draft');
}
- The property hooks run for the view's changes:
nitro:model,$set(), and in a JavaScript view,set(),v-modelor a binding. A method that assigns a property doesn't run them: the method already knows what it changed. updating*()runs before the change, so$this->searchstill holds the old value and the hook gets the new one;updated*()runs after it. A property's own hook runs first, thenupdating()orupdated().- A hook names its property in StudlyCase:
updatedFirstName()is$firstName's, or$first_name's. A method whose name names no property, such asupdatedAt(), is an ordinary method. - For a key under an array,
updatedFilters()gets the new value of that key, and the key's path (status);updated()gets the full path (filters.status). rendered()runs after each render that changed the view, not the first: that ismounted(). What it changes renders too, without running it again, so it can't loop. Use it to keep a third-party widget in step with the view.- An error in a hook is reported in the console, naming the component; the component carries on.
When a hook needs the server
Mark updated*(), mounted() or destroyed() #[Server] and it runs
on the server: the browser makes the change, then calls the hook, sending the change along. It is an ordinary
#[Server] method, with its rules and checks:
#[Validate('required|alpha_dash|min:3|max:30')]
public string $username = '';
public string $availability = '';
/**
* On the server, after the view changes $username (nitro:model.debounce.400ms="username").
* Its #[Validate] rule is checked first: a name that fails it never gets here.
*/
#[Server]
public function updatedUsername(string $value): void
{
$this->availability = User::where('username', $value)->exists() ? 'Taken' : 'Available';
}
Pair it with nitro:model.debounce, so it runs once the visitor stops typing rather than on every
key. Two hooks stay in the browser, and #[Server] on them is an error that says so:
updating*() runs before the change, which can't wait for a request, and rendered()
runs after every render, which would make each render a request. Have them call a #[Server]
method instead.
When a server method throws
exception() runs on the server when a #[Server] method throws. The call then answers
normally, with the state as the hook left it, so the view can say what went wrong. To let the error through
after all, throw it again. A failed validate() isn't an exception here: its messages go to
$errors as always.
public string $problem = '';
#[Server]
public function sync(): void
{
$this->problem = '';
$this->contacts = Crm::contacts()->since($this->lastSync)->get()->toArray();
}
/** A #[Server] method threw: tell the visitor, report it, and keep the page working. */
public function exception(Throwable $exception): void
{
if (! $exception instanceof CrmUnavailable) {
throw $exception;
}
report($exception);
$this->problem = 'The CRM is not answering. Try again in a minute.';
}
An error the hook throws again, or one in a component without the hook, shows the component's fallback in production. See Handling errors.
Browser hooks protect nothing
updated*() runs in the browser, so a visitor can skip it and send any value of a property's type. Keep what must hold on the server in #[Validate] rules, #[Locked] properties and #[Server] methods. See Security.
After the next render
To run JavaScript once the view has rendered, as a hook would, call $this->js(). The snippet can
use $nitro (the component), $refs (its elements marked with nitro:ref)
and $el (its root element):
public function add(): void
{
$this->items[] = $this->draft;
$this->draft = '';
// After the next render: focus the input again.
$this->js('$refs.draft.focus()');
}
From a #[Server] method, it runs once the answer has rendered. The snippet is code: never build it
from what a visitor typed.