Nitro

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.

A component and its children live, in this page View source ↓

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):

Terminal
php artisan make:nitro FaqItem

Then fill them in:

Browser + server
<?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;
    }
}
  • 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:

Terminal
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.

Terminal
  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.show and Posts\Show all make App\Nitro\Pages\Posts\Show. Each part becomes StudlyCase.
  • Where files go is what config/nitro.php says (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; --force writes over it.
  • Your own templates. Publish them with php artisan vendor:publish --tag=nitro-stubs, and edit stubs/nitro.component.stub, nitro.page.stub, nitro.store.stub or nitro.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:

PHP
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:

resources/views/nitro/components/faq.blade.php
@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:

Blade
{{-- 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-price is $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:

app/Nitro/Components/OrderLine.php Browser + server
<?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:

app/Nitro/Components/PostSummary.php
<?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:

Blade
<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'] }}:

Blade
{{-- 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>
Blade
{{-- 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>
app/Nitro/Components/Panel.php Browser + server
<?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: $members and invite above 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]:

app/Nitro/Components/DatePicker.php Browser + server
<?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;
    }
}
Blade
{{-- 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:

Blade
{{-- 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:model on 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:

PHP
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.

Next steps