Essentials
Components
A component is a PHP class and its view. The class holds the state and the behaviour; the view shows them. You place it anywhere in Blade with a tag, and it runs in the browser from then on, going to the server only for what needs it.
On this page
A component is one part of a page: a search box, a cart, a table, a date picker. Its public properties are its
state. Its methods run in the browser, compiled from the PHP you wrote, so most interactions need no request.
Methods marked #[Server] run on the server, with the database, mail and anything else Laravel
has. The view, Blade by default, renders on the server first and in the browser after, from the same template.
Components and pages
Nitro has two kinds of classes, written the same way. A component is placed in a view. A page is a whole screen with a route, a layout and a title (see Pages). Everything on this page is true of both.
| Components | Pages | |
|---|---|---|
| Class |
app/Nitro/Components
|
app/Nitro/Pages
|
| View |
resources/views/nitro/components
|
resources/views/nitro/pages
|
| Name |
posts.table
for
App\Nitro\Components\Posts\Table
|
pages::posts.index
for
App\Nitro\Pages\Posts\Index
|
| Used |
placed in a view:
<nitro:posts.table />
|
routed:
Route::nitro('/posts', Index::class)
|
| Layout and title | no |
#[Layout]
,
#[Title]
|
You can't route a component or place a page inside a view; Nitro's error says what to use instead. The
directories, namespaces and view paths are in config/nitro.php.
Creating a component
A component is a class that extends Nitro\Component in app/Nitro/Components, and
its view in resources/views/nitro/components, named after the class in kebab case:
FaqItem has faq-item.blade.php. make:nitro writes both
(more below):
php artisan make:nitro FaqItem
Then fill them in:
<?php
namespace App\Nitro\Components;
use Nitro\Component;
class FaqItem extends Component
{
public string $question = '';
public string $answer = '';
public bool $open = false;
public function mount(string $question, string $answer): void
{
$this->question = $question;
$this->answer = $answer;
}
public function toggle(): void
{
$this->open = ! $this->open;
}
}
<div class="px-4">
<button type="button" nitro:click="toggle" class="flex w-full items-center justify-between gap-4 py-3 text-left text-sm font-medium text-slate-900 dark:text-white">
{{ $question }}
<span @class(['text-slate-400 transition', 'rotate-45' => $open])>+</span>
</button>
@if ($open)
<p class="pb-3 text-sm text-slate-600 dark:text-slate-400">{{ $answer }}</p>
@endif
</div>
Compiling…
- The public properties (
$question,$answer,$open) are its state: rendered by the server, sent to the browser, and sent back with each#[Server]call. toggle()is a plain method, so it runs in the browser: opening an answer sends nothing to the server.- The view has a single root element. Nitro marks it with the component's id and state.
make:nitro
One command makes a component, a page or a store, with its view when it has one:
php artisan make:nitro FaqItem # a component: its class and its view
php artisan make:nitro Posts/Show --page # a page: its class (with #[Title] and mount()) and its view
php artisan make:nitro Cart --store # a store: its class only
It says what it wrote, and what to do next: the tag to place a component with, the route for a page.
INFO Created [app/Nitro/Pages/Posts/Show.php].
INFO Created [resources/views/nitro/pages/posts/show.blade.php].
Route it: Route::nitro('/posts/show', \App\Nitro\Pages\Posts\Show::class);
- Folders are part of the name:
Posts/Show,posts.showandPosts\Showall makeApp\Nitro\Pages\Posts\Show. Each part becomes StudlyCase. - Where files go is what
config/nitro.phpsays (components,pages,stores), the same places Nitro finds them. - Nothing is written over. When a file is there already, the command stops and names it;
--forcewrites over it. - Your own templates. Publish them with
php artisan vendor:publish --tag=nitro-stubs, and editstubs/nitro.component.stub,nitro.page.stub,nitro.store.stubornitro.view.stub: the command uses yours. - A JavaScript view (Vue, React, Solid, Svelte) isn't made for you yet: make the component, delete its Blade view, and add
#[View](see JavaScript views).
Names and folders
A component's name comes from its class's place under app/Nitro/Components: each folder is a
dot, and each part is kebab case. The name is its tag and its view's path:
| Class | Name | Tag | View |
|---|---|---|---|
App\Nitro\Components\Counter
|
counter
|
<nitro:counter />
|
nitro/components/counter.blade.php
|
App\Nitro\Components\FaqItem
|
faq-item
|
<nitro:faq-item />
|
nitro/components/faq-item.blade.php
|
App\Nitro\Components\Posts\Table
|
posts.table
|
<nitro:posts.table />
|
nitro/components/posts/table.blade.php
|
#[View] gives a component another view: a Blade view elsewhere, shared by several classes, or a
view written in Vue, React, Solid or Svelte:
use Nitro\Attributes\View;
#[View('shared.address-form')] // resources/views/shared/address-form.blade.php
class ShippingAddress extends Component { /* ... */ }
#[View(vue: 'billing/address')] // resources/views/vue/billing/address.vue
class BillingAddress extends Component { /* ... */ }
Placing a component
A <nitro:...> tag places a component in any Blade view: a page, a layout, a plain Laravel
view, or another component's view, where it is a child. Each tag is its own instance with its
own state, so opening one answer leaves the others closed:
@foreach ($questions as $i => $item)
<nitro:faq-item :question="$item['question']" :answer="$item['answer']" :key="$i" />
@endforeach
A tag inside PHP (an @php block, an echo, a string), a Blade comment or @verbatim
is text, not a component. In a Blade view that isn't a component's, such as a plain Laravel page, a tag is an
island, which can start later: when it is visible, when the browser is idle,
or on interaction.
Passing data to a component
The tag's attributes are the values the component starts with:
{{-- resources/views/nitro/components/order.blade.php --}}
<div>
<nitro:order-line
label="Gift wrap"
:quantity="$quantity"
:unit-price="$product->price_cents"
:editable="! $locked"
/>
</div>
label="Gift wrap"is a string, as written.:quantity="$quantity"is a PHP expression, worked out in the parent's view: any value, a model, an array.- Kebab case in the tag is camel case in the class:
:unit-priceis$unitPrice.
Where those values go depends on whether the component has a mount() method.
Into its properties
Without mount(), each value sets the public property of its name. A name the class doesn't have
is an error, so a typo in a tag is caught where it is written:
<?php
namespace App\Nitro\Components;
use Nitro\Component;
/** No mount(): the tag's values set these properties, and follow the parent when they change. */
class OrderLine extends Component
{
public string $label = '';
public int $quantity = 1;
public int $unitPrice = 0;
public bool $editable = true;
public function total(): int
{
return $this->quantity * $this->unitPrice;
}
}
These values follow the parent: when the parent's $quantity changes and its view
renders again, the line's $quantity changes with it. And because nothing has to run on the server,
the browser can create such a child by itself, when a loop gets a new item or an @if becomes
true.
Into mount()
With mount(), the values are its arguments, by name. It runs once, on the server, when the
component is created, and it can do server work: load a model, check a policy, call a service. Laravel's
container calls it, so anything it can give, it can take, as a controller does:
<?php
namespace App\Nitro\Components;
use App\Models\Post;
use App\Services\ReadingTime;
use Nitro\Attributes\Locked;
use Nitro\Component;
class PostSummary extends Component
{
#[Locked]
public int $postId;
public string $readingTime = '';
public array $comments = [];
/**
* Runs once, on the server: the tag's values by name (:post="$post"), and anything else
* Laravel's container can give, as in a controller.
*/
public function mount(Post $post, ReadingTime $readingTime): void
{
$this->postId = $post->id;
$this->readingTime = $readingTime->of($post->body);
$this->comments = $post->comments()->latest()->take(3)->get(['author', 'body'])->toArray();
}
}
A page's mount() takes the route's parameters the same way, with route model binding. See
Pages.
| No mount() | mount() | |
|---|---|---|
| Where the values go | The public properties of their names |
mount()
's parameters, by name
|
| When the parent's values change | The properties follow them |
Nothing:
mount()
ran once
|
| Server work at creation | None | Anything: models, policies, services |
| Created in the browser |
Yes: a new loop item, an
@if
that turns true
|
No: only the server can run
mount()
|
Which to choose
Pass plain values as properties: a child the browser can make and keep in step costs no request. Use mount() when creating the component needs the server, as PostSummary does to load its comments. A child with mount() is made by the server only: when its loop gets a new item, add the item in a #[Server] method, whose answer brings the new child with it. A browser method that adds one reports an error in the console instead.
Keys and lists
Nitro tells children apart by their place in the view: the first tag, the second, and inside a loop, its
index. That is enough while a list only grows at the end. When it is sorted, filtered or added to in the
middle, give each child a :key from its data, so each keeps its own state as it moves:
<ul>
@foreach ($tasks as $task)
{{-- Sorted, filtered or added to: each child keeps its own state by its task's id. --}}
<nitro:task-item :task="$task" :key="$task['id']" />
@endforeach
</ul>
A key is unique among the parent's children. The server and the browser number children the same way, so a child keeps its state when the browser takes over from the server's HTML.
Slots
What you put between a component's tags is its slot. It renders inside the child, where the
child's view prints {{ $slot }}. Named slots go in <nitro:slot name="...">
and are printed with {{ $slots['name'] }}:
{{-- resources/views/nitro/components/team.blade.php --}}
<div>
<nitro:panel :collapsible="true">
{{-- The default slot: rendered here, with this view's values and methods. --}}
<p>{{ count($members) }} members</p>
<button nitro:click="invite">Invite someone</button>
<nitro:slot name="footer">
<a href="/team/settings" nitro:navigate>Team settings</a>
</nitro:slot>
</nitro:panel>
</div>
{{-- resources/views/nitro/components/panel.blade.php --}}
<section class="panel">
@if ($collapsible)
<button nitro:click="$toggle('open')">{{ $open ? 'Hide' : 'Show' }}</button>
@endif
@if ($open)
<div>{{ $slot }}</div>
@endif
@if ($slots->has('footer'))
<footer>{{ $slots['footer'] }}</footer>
@endif
</section>
<?php
namespace App\Nitro\Components;
use Nitro\Component;
class Panel extends Component
{
public bool $collapsible = false;
public bool $open = true;
}
- A slot belongs to the parent. It renders with the parent's values and methods:
$membersandinviteabove are the team's, not the panel's. When the team's state changes, the slot updates inside the panel. - The child decides where, and whether: the panel shows its slot only while
$open. $slots->has('footer')says whether a named slot was passed with content;$slot->isEmpty()the same for the default one.
Binding a child like an input
A component of your own can be bound with nitro:model, as an <input> is. Mark
the property that holds its value with #[Modelable]:
<?php
namespace App\Nitro\Components;
use Nitro\Attributes\Modelable;
use Nitro\Component;
/** A date input of your own, bound by its parent like any input. */
class DatePicker extends Component
{
#[Modelable]
public string $value = '';
public bool $open = false;
public function pick(string $date): void
{
$this->value = $date; // the parent's property changes with it
$this->open = false;
}
}
{{-- In the parent's view: its $startsOn, and a key under an array --}}
<nitro:date-picker nitro:model="startsOn" />
<nitro:date-picker nitro:model="booking.endsOn" />
@foreach ($stops as $i => $stop)
<nitro:date-picker nitro:model="stops.{{ $i }}.date" :key="$stop['id']" />
@endforeach
The child's $value starts as the parent's property, follows it when it changes, and writes it back
when the child changes it: pick() sets the parent's $startsOn, in the browser, with no
request. The path can be a key under an array, built in a loop (stops.{{ $i }}.date). A component
has one #[Modelable] property.
Dynamic components
<nitro:dynamic-component :is="..."> places the component an expression names, and changes
it when the expression does. Tabs and wizards are the usual case:
{{-- resources/views/nitro/components/settings.blade.php --}}
<div>
<nav>
<button nitro:click="$set('tab', 'settings.profile')">Profile</button>
<button nitro:click="$set('tab', 'settings.billing')">Billing</button>
<button nitro:click="$set('tab', 'settings.notifications')">Notifications</button>
</nav>
{{-- The component $tab names; '' renders nothing. Another name, another component. --}}
<nitro:dynamic-component :is="$tab" />
</div>
- While the name stays the same, the component stays, with its state. Another name is another component: the old one goes, and its state with it.
- An empty string renders nothing. A name that isn't a component, or names a page, is an error.
- Other attributes on the tag are passed to whichever component it places.
Don't let visitors name it
The name is a value in the component's state, which the browser can change. Keep the choice to a list your code checks, such as a match on a tab's key, or #[Locked] when the server picks it.
Components that work together
Components are independent: each has its own state, and none reaches into another's. They work together in three ways:
- Down, through the tag: the values a parent passes, and its slots.
- Up, with
nitro:modelon a#[Modelable]child, or across, with events: one dispatches, any others listen with#[On], wherever they are on the page. - Shared, with stores: state several components read and change, such as a cart.
Where your code runs
The server renders a component first, so the page arrives complete and search engines read it. The browser then takes over and runs the compiled class and view from that point on:
| Code | Runs |
|---|---|
mount()
,
boot()
,
hydrate()
,
dehydrate()
,
exception()
|
On the server |
#[Server]
methods, and hooks marked
#[Server]
|
On the server, when the browser calls them |
Other methods,
#[Computed]
methods,
updating*()
and
updated*()
hooks,
mounted()
,
rendered()
,
destroyed()
|
In the browser |
| The view | On the server for the first render, then in the browser |
Server-only code can use anything
Nitro compiles only the methods the browser can reach. A private helper called only from mount() or a #[Server] method is never compiled, so it can use anything PHP can: Eloquent, facades, packages.
Each step of that life, and the hooks you can use at each, are on Lifecycle.
Loading a component's code later
Each component's compiled code is in your app's bundle, and pages are chunks of their own. A large component
that few pages show can be a chunk too, loaded when a page first needs it, with #[LazyLoad].
Components that are shown together can share one:
use Nitro\Attributes\LazyLoad;
#[LazyLoad] // its own chunk, loaded when a page first shows it
class ReportBuilder extends Component { /* ... */ }
#[LazyLoad('charts')] // one chunk for the three: one request for whichever shows first
class RevenueChart extends Component { /* ... */ }
#[LazyLoad('charts')]
class SignupsChart extends Component { /* ... */ }
#[LazyLoad('charts')]
class ChurnChart extends Component { /* ... */ }
A page rendered on the server lists the chunks it uses, so the browser loads them at once, and a visit loads a page's chunks while it fetches its data. Neither waits for the other.