Introduction

Inertia lets you build modern single-page applications with Laravel's server-side routing and Vue or React page components. No REST API, no client-side router — just controllers returning page responses.

Key Concepts

  • Inertia::render: The controller helper that returns a page component and its props instead of a Blade view.
  • Page component: A Vue or React component in resources/js/Pages/ corresponding to a route.
  • <Link>: Inertia's client-side navigation component that swaps pages via XHR without a full reload.
  • Props: Data passed from controller to page component via the second argument of Inertia::render.
  • Shared data: Values merged into every page's props via the HandleInertiaRequests middleware (authenticated user, flash messages, feature flags).

Real World Context

When you want SPA smoothness (instant navigation, no full reloads, scroll preservation) but don't want to build and maintain a separate JSON API, Inertia is the pragmatic choice. Controllers stay classic Laravel; pages become Vue/React components.

Deep Dive

Inertia.js lets you build modern single-page applications using server-side routing and controllers. No need for an API—connect your Laravel backend directly to Vue or React.

What is Inertia?

Inertia is the glue between your Laravel backend and JavaScript frontend:

┌─────────────────────────────────────────────────────────────────┐
│                    Traditional SPA                               │
│                                                                 │
│  Frontend (React/Vue)  ←──── API ────→  Backend (Laravel)       │
│       (routing,                         (controllers,           │
│        state,                            business logic,        │
│        components)                       database)              │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                    Inertia.js                                    │
│                                                                 │
│  Frontend (React/Vue)  ←── Inertia ──→  Backend (Laravel)       │
│       (components                        (controllers,          │
│        only!)                            routing,               │
│                                          business logic,        │
│                                          database)              │
└─────────────────────────────────────────────────────────────────┘

With Inertia:

  • No API to build
  • Server-side routing (like classic Laravel)
  • Full SPA experience
  • Use Vue or React components
  • SEO friendly (server-side rendering available)

Installation with a Starter Kit

Laravel 13 replaced Breeze with first-party starter kits that scaffold the frontend, routing, and authentication flows in one step. Pick one when you create the project:

bash
laravel new my-app

# Choose one of:
# - React   (Inertia 3 + React)
# - Vue     (Inertia 3 + Vue)
# - Svelte  (Inertia 3 + Svelte)
# - Livewire (full-stack PHP with Livewire 4)

The three Inertia kits ship with Inertia 3, Tailwind v4, and authentication scaffolding preconfigured; the Livewire kit is the alternative for teams who want to stay in Blade.

Manual Installation

bash
composer require inertiajs/inertia-laravel
npm install @inertiajs/vue3  # or @inertiajs/react

How It Works

Server Side (Laravel)

php
// routes/web.php
Route::get('/users', [UserController::class, 'index']);
Route::get('/users/{user}', [UserController::class, 'show']);
Route::post('/users', [UserController::class, 'store']);
php
// app/Http/Controllers/UserController.php
use Inertia\Inertia;

class UserController extends Controller
{
    public function index()
    {
        return Inertia::render('Users/Index', [
            'users' => User::all(),
        ]);
    }

    public function show(User $user)
    {
        return Inertia::render('Users/Show', [
            'user' => $user,
        ]);
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required',
            'email' => 'required|email|unique:users',
        ]);

        User::create($validated);

        return redirect()->route('users.index');
    }
}

Client Side (Vue)

vue
<!-- resources/js/Pages/Users/Index.vue -->
<script setup>
import { Link } from '@inertiajs/vue3'

defineProps({
    users: Array
})
</script>

<template>
    <h1>Users</h1>

    <Link href="/users/create">Create User</Link>

    <ul>
        <li v-for="user in users" :key="user.id">
            <Link :href="`/users/${user.id}`">{{ user.name }}</Link>
        </li>
    </ul>
</template>
vue
<!-- resources/js/Pages/Users/Show.vue -->
<script setup>
defineProps({
    user: Object
})
</script>

<template>
    <h1>{{ user.name }}</h1>
    <p>{{ user.email }}</p>
</template>

Inertia's <Link> replaces anchor tags for SPA navigation:

vue
<script setup>
import { Link } from '@inertiajs/vue3'
</script>

<template>
    <!-- Basic link -->
    <Link href="/users">Users</Link>

    <!-- With method -->
    <Link href="/logout" method="post" as="button">Logout</Link>

    <!-- Preserve scroll position -->
    <Link href="/users" preserve-scroll>Users</Link>

    <!-- With data -->
    <Link href="/users" :data="{ page: 2 }">Page 2</Link>
</template>

Forms

Use Inertia's form helper:

vue
<script setup>
import { useForm } from '@inertiajs/vue3'

const form = useForm({
    name: '',
    email: '',
})

const submit = () => {
    form.post('/users')
}
</script>

<template>
    <form @submit.prevent="submit">
        <div>
            <input v-model="form.name" placeholder="Name">
            <span v-if="form.errors.name">{{ form.errors.name }}</span>
        </div>

        <div>
            <input v-model="form.email" type="email" placeholder="Email">
            <span v-if="form.errors.email">{{ form.errors.email }}</span>
        </div>

        <button type="submit" :disabled="form.processing">
            {{ form.processing ? 'Saving...' : 'Create User' }}
        </button>
    </form>
</template>

Shared Data

Share data with all pages via middleware:

php
// app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
    return array_merge(parent::share($request), [
        'auth' => [
            'user' => $request->user(),
        ],
        'flash' => [
            'success' => $request->session()->get('success'),
            'error' => $request->session()->get('error'),
        ],
    ]);
}

Access in components:

vue
<script setup>
import { usePage } from '@inertiajs/vue3'

const page = usePage()
const user = page.props.auth.user
const flash = page.props.flash
</script>

<template>
    <div v-if="flash.success" class="alert-success">
        {{ flash.success }}
    </div>

    <p v-if="user">Welcome, {{ user.name }}</p>
</template>

Common Pitfalls

  1. Using <a> tags for internal navigation — Full page reloads, no preserved state. Always use <Link>.
  2. Returning view() instead of Inertia::render() — The controller returns a Blade template instead of an Inertia response, breaking client-side navigation.
  3. Mismatching page component paths — Inertia::render('Users/Index') looks for resources/js/Pages/Users/Index.vue (or .tsx). Capitalization and forward slashes matter.

Best Practices

  1. Use <Link> for all internal navigation — It preserves scroll, supports prefetching, and exposes method/data props for non-GET links.
  2. Organize Pages/* by feature — Mirror the route structure so users.index maps to Pages/Users/Index.
  3. Centralize cross-page values with shared data — Auth, flash messages, feature flags, CSRF token all belong in HandleInertiaRequests::share().

Summary

  • Inertia bridges Laravel's server-side routing with Vue/React page components.
  • Controllers return Inertia::render('Pages/Name', $props) instead of views.
  • <Link> performs client-side navigation; regular <a> tags trigger full reloads.
  • Forms use useForm() for reactive state, server-side validation, and loading flags.
  • Laravel 13 starter kits ship with Inertia 3 and the React/Vue page structure pre-configured.
✓ Completed