Nitro

Getting started

Coming from Inertia

If you know Inertia, you know most of Nitro already: Laravel routes, pages rendered by the server first, visits that fetch only data, shared and deferred props. The difference is where a page lives. In Inertia it is a controller and a Vue or React page; in Nitro it is one PHP class, and its view can be Blade or Vue, React, Solid or Svelte.

On this page

A page is its own controller

An Inertia page is two things: a controller action that gathers props and calls Inertia::render(), and a JavaScript page that receives them. Every change goes back through another route and controller: a form posts, the controller validates and redirects back, and the page receives its props again.

A Nitro page is one class. Its public properties are the props, mount() is the controller action, and its methods are the actions on the page. There is no Nitro::render(): you route the class, and Nitro renders it.

The same page, both ways

A post with a comment form. In Inertia, two routes, two controllers and a page:

Inertia: routes and controllers
// routes/web.php
Route::get('/posts/{post}', [PostController::class, 'show']);
Route::post('/posts/{post}/comments', [CommentController::class, 'store']);

// app/Http/Controllers/PostController.php
class PostController extends Controller
{
    public function show(Post $post)
    {
        return Inertia::render('Posts/Show', [
            'post' => $post->only('id', 'title', 'body'),
        ]);
    }
}

// app/Http/Controllers/CommentController.php
class CommentController extends Controller
{
    public function store(Request $request, Post $post)
    {
        $request->validate(['comment' => 'required|max:1000']);
        $post->comments()->create(['body' => $request->comment, 'user_id' => auth()->id()]);

        return back();
    }
}
Inertia: resources/js/Pages/Posts/Show.vue
<script setup>
import { useForm } from '@inertiajs/vue3';

const props = defineProps({ post: Object });
const form = useForm({ comment: '' });

function submit() {
    form.post(`/posts/${props.post.id}/comments`, { onSuccess: () => form.reset() });
}
</script>

<template>
    <article>
        <h1>{{ post.title }}</h1>
        <p>{{ post.body }}</p>

        <form @submit.prevent="submit">
            <textarea v-model="form.comment"></textarea>
            <p v-if="form.errors.comment" class="error">{{ form.errors.comment }}</p>
            <button :disabled="form.processing">Comment</button>
        </form>
    </article>
</template>

In Nitro, one route and one class, with its view:

routes/web.php
Route::nitro('/posts/{post}', Show::class)->name('posts.show');
app/Nitro/Pages/Posts/Show.php Browser + server
<?php

namespace App\Nitro\Pages\Posts;

use App\Models\Post;
use Nitro\Attributes\Locked;
use Nitro\Attributes\Server;
use Nitro\Attributes\Title;
use Nitro\Attributes\Validate;
use Nitro\Attributes\View;
use Nitro\Component;

#[Title('Post')]
#[View(vue: 'posts/show')]
class Show extends Component
{
    #[Locked]
    public array $post = [];

    #[Validate('required|max:1000')]
    public string $comment = '';

    public function mount(Post $post): void
    {
        $this->post = $post->only('id', 'title', 'body');
    }

    #[Server]
    public function addComment(): void
    {
        $this->validate();
        Post::findOrFail($this->post['id'])->comments()->create(['body' => $this->comment, 'user_id' => auth()->id()]);
        $this->reset('comment');
    }
}
resources/views/vue/posts/show.vue
<template>
    <article>
        <h1>{{ post.title }}</h1>
        <p>{{ post.body }}</p>

        <form @submit.prevent="addComment()">
            <textarea v-model="comment"></textarea>
            <p v-if="errors.comment" class="error">{{ errors.comment[0] }}</p>
            <button :disabled="addComment.processing">Comment</button>
        </form>
    </article>
</template>
  • No props to declare. The view reads the page's properties by their PHP names: post, comment, errors. v-model writes them.
  • No form helper. The form's fields are properties, and addComment() is the submit. addComment.processing is what form.processing was, and errors holds the messages.
  • No second route. #[Server] methods are called on the page itself, with its state signed by the server. #[Locked] keeps the browser from changing post.
  • No redirect back. The page stays where it is: the call changes comment, and the view shows it.

The view could be Blade instead, with the same class: drop vue: from #[View]. Pages with Blade views run their browser methods without any framework at all, and a Blade page can place components with Vue, React, Solid or Svelte views side by side (see JavaScript views).

See it running

The same kind of page, a blog searched in the browser and written on the server, built four times: with a Vue, React, Solid and Svelte view. Their links visit each other without a reload.

Inertia's features in Nitro

The server side:

Inertia Nitro Read
Inertia::render('Posts/Show', [...]) A page class: its public properties, set in mount() Pages
Inertia::share() Nitro::share() , read with $this->shared() Page data
Inertia::once() , shareOnce() Nitro::once() , Nitro::shareOnce() Page data
Inertia::flash() Nitro::flash() , read with $this->flashed() Page data
Inertia::defer() $this->defer('stats', fn () => ...) in mount() Page data
Inertia::merge() , deepMerge() $this->merge() , $this->deepMerge() Page data
Inertia::scroll() $this->scroll() with a paginator, $loadMore() in the view Page data
Partial reloads ( only ) $this->reload('orders') or $reload('orders') ; $this->wants() skips the work a reload doesn't need Page data
Asset versioning The build's version: a visit from another build loads the page in full Building for production
<Head> , title #[Title] , $this->head() The page's head
redirect() from a controller $this->redirect($url, navigate: true) in a method Server methods
Validation errors in props #[Validate] , $this->validate() , $errors in the view Forms and validation
Testing helpers assertNitro() on a page's response, callNitro() for a #[Server] method Testing

The browser side:

Inertia Nitro Read
<Link href> <a nitro:navigate href> ; visit(url) in a Vue view Navigation
router.visit() $visit() in Blade, visit() in a JavaScript view Navigation
router.reload({ only }) $reload('orders') Page data
Link prefetching nitro:prefetch , on hover or .visible Navigation
The progress bar Nitro's progress bar, or your own Navigation
View transitions view_transitions , or .transition on a link Navigation
useForm() The page's properties, #[Validate] , a #[Server] method and its processing Forms and validation
form.isDirty nitro:dirty in Blade, unsaved in a JavaScript view Forms and validation
useRemember() #[Remember] on a property Properties
usePoll() nitro:poll.30s="refresh" Directives
<WhenVisible> nitro:visible="loadMore" Directives
<InfiniteScroll> $this->scroll() and nitro:visible="$loadMore('posts')" Page data

Read more in Pages, Page data, Navigation, The page's head, Forms and validation, Server methods, Properties, Directives and Testing.

What Nitro adds

  • Methods that run in the browser, written in PHP. A method without #[Server] is compiled to JavaScript and runs on a click with no request. In Inertia, that logic is written again in the page's JavaScript. See Methods.
  • Server calls without routes. A #[Server] method is called from the view by name, with its arguments and its return value, and the page's state travels with it, signed. See Server methods.
  • Validation as fields are left. #[Validate(onBlur: true)] checks a field on the server when the visitor leaves it, before the form is sent. See Forms and validation.
  • Components, not only pages. A search box, a cart or a table is a component with its own state and methods, placed on any page. See Components.
  • Stores. State shared between components, per tab or per request, with methods compiled like a component's. See Sharing state.
  • Blade views that run in the browser. No framework needed for a page that doesn't want one; islands and server-made HTML for the parts that should stay on the server. See Islands and Server-made HTML.
  • Mistakes fail the build. A browser method that queries the database, or a reserved name, is an error naming the file and line. See How Nitro works.

What is different

Some of Inertia's features have no counterpart in Nitro:

Inertia In Nitro
Inertia::optional() A prop left out of the first load and sent only when asked for. Nitro's wants() skips work on a reload of other properties, not on the first load; defer() loads the property right after the page shows
Inertia::always() A prop sent with every partial reload. A Nitro reload takes the properties you name
encryptHistory() , clearHistory() Encrypting the history entries' data. Don't put secrets in a #[Remember] property
preserveFragment Keeping the #fragment across a redirect
Turning server rendering off per request Server rendering is on or off for the app, in nitro.ssr.enabled

And some things work differently:

  • Posting forms to routes. You can, but you don't need to: a #[Server] method replaces the route, the controller and the redirect back.
  • One Vue app per page. Inertia mounts one app and swaps pages inside it. In Nitro each component with a Vue view is its own app; share state between them with stores, events or one Pinia (see Vue).
  • A JavaScript view can't place Nitro components. Compose components from Blade: a Blade page or layout can hold several, whatever their views.

Next steps