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:
// 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();
}
}
<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:
Route::nitro('/posts/{post}', Show::class)->name('posts.show');
<?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');
}
}
<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-modelwrites them. - No form helper. The form's fields are properties, and
addComment()is the submit.addComment.processingis whatform.processingwas, anderrorsholds 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 changingpost. - 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).
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.