DEV Community

Cover image for Dev.to — Laravel with React or Vue through Inertia
Sumeet Shroff
Sumeet Shroff

Posted on

Dev.to — Laravel with React or Vue through Inertia

Laravel with React or Vue through Inertia

If you've spent time with Laravel and wondered how to use React or Vue without abandoning the monolith, Inertia.js is the answer. It bridges the gap between a server-driven Laravel backend and a client-rendered component frontend — no separate API, no JSON contract negotiation, no duplicated routing layer.

This article focuses on the practical setup: how Inertia actually works, how to scaffold it with React or Vue, and where it makes sense compared to going full-SPA or staying with Blade.


Prerequisites and Versions

Before starting, confirm your stack:

  • Laravel 11 or 12 — required for the Inertia Laravel adapter v3
  • Inertia.js v3.0.0 (stable since March 25, 2026) — current default
  • Inertia.js v2.x — still supported; official Laravel starter kits ship with v2
  • React 19 or Vue 3 — both work with Inertia v2 and v3
  • Node.js 20+ and Vite for the frontend build pipeline
  • Composer and pnpm (or npm)

If you are on Laravel 10, you are capped at Inertia adapter v2. The v3 adapter requires Laravel 11 minimum.


How Inertia.js Works (The Mental Model)

Inertia is not a UI framework. It is a protocol adapter that sits between Laravel controllers and React/Vue components.

On a full-page load, Laravel renders an HTML shell with a single <div id="app"> that contains a JSON payload in a data-page attribute. Inertia's client-side adapter bootstraps React or Vue, reads that payload, and renders the correct page component.

On subsequent navigations, clicking an <Link> component triggers a fetch request with an X-Inertia: true header. Laravel detects this header and returns only JSON — the component name and its props — instead of a full HTML page. The client swaps the component without a full browser reload.

The result: your Laravel routes, controllers, and auth middleware work exactly as they always did. Your frontend developers get full React or Vue components with props typed exactly as the controller passes them.


Scaffolding a New Project

Option A — Official Laravel Starter Kit (Fastest)

# React 19 + Inertia 2 + Tailwind 4 + shadcn/ui
laravel new my-app --react

# Vue 3 + Inertia 2 + Tailwind 4
laravel new my-app --vue
Enter fullscreen mode Exit fullscreen mode

Note: as of mid-2026, the official starter kits ship with Inertia v2, not v3. Inertia v3 must be manually upgraded after scaffolding if needed.

Option B — Breeze with Inertia Stack

composer require laravel/breeze --dev
php artisan breeze:install react
# or
php artisan breeze:install vue

npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

Breeze installs authentication scaffolding (login, register, password reset) wired through Inertia pages. It's a solid starting point for applications that need auth out of the box.

Option C — Manual Inertia Install

For projects where you need fine-grained control:

# Server side
composer require inertiajs/inertia-laravel

# Publish middleware
php artisan inertia:middleware
# Register HandleInertiaRequests in bootstrap/app.php

# Client side — React
npm install @inertiajs/react react react-dom

# Or Vue
npm install @inertiajs/vue3 vue
Enter fullscreen mode Exit fullscreen mode

Root template (resources/views/app.blade.php):

<!DOCTYPE html>
<html>
<head>
    @viteReactRefresh {{-- React only --}}
    @vite(['resources/js/app.jsx', 'resources/css/app.css'])
    @inertiaHead
</head>
<body>
    @inertia
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

React entrypoint (resources/js/app.jsx):

import { createInertiaApp } from '@inertiajs/react';
import { createRoot } from 'react-dom/client';

createInertiaApp({
    resolve: name => {
        const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true });
        return pages[`./Pages/${name}.jsx`];
    },
    setup({ el, App, props }) {
        createRoot(el).render(<App {...props} />);
    },
});
Enter fullscreen mode Exit fullscreen mode

Vue entrypoint (resources/js/app.js):

import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';

createInertiaApp({
    resolve: name => {
        const pages = import.meta.glob('./Pages/**/*.vue', { eager: true });
        return pages[`./Pages/${name}.vue`];
    },
    setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
            .use(plugin)
            .mount(el);
    },
});
Enter fullscreen mode Exit fullscreen mode

Writing Controllers and Page Components

A Laravel controller renders an Inertia page the same way it would render a Blade view — with one method call:

use Inertia\Inertia;

class ProductController extends Controller
{
    public function index()
    {
        return Inertia::render('Products/Index', [
            'products' => Product::latest()->paginate(20),
        ]);
    }

    public function show(Product $product)
    {
        return Inertia::render('Products/Show', [
            'product' => $product->load('reviews'),
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

The matching React page component at resources/js/Pages/Products/Index.jsx:

export default function Index({ products }) {
    return (
        <div>
            <h1>Products</h1>
            {products.data.map(product => (
                <div key={product.id}>{product.name}</div>
            ))}
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

Props flow one way: controller → component. There is no need to define API endpoints, serializers, or fetch logic for standard page loads.


Inertia v2 Features Worth Using

Deferred Props

For data that is expensive to compute and not needed for the initial render:

// Controller
return Inertia::render('Dashboard', [
    'user'     => $user,
    'stats'    => Inertia::defer(fn () => $this->getStats()),
    'activity' => Inertia::defer(fn () => $this->getActivity(), 'secondary'),
]);
Enter fullscreen mode Exit fullscreen mode
// React component
import { Deferred } from '@inertiajs/react';

<Deferred data="stats" fallback={<p>Loading stats...</p>}>
    {({ stats }) => <StatsChart data={stats} />}
</Deferred>
Enter fullscreen mode Exit fullscreen mode

stats loads in a second request after the page renders. Props in the same group string ('secondary') are fetched in a single batched request.

Prefetching

import { Link } from '@inertiajs/react';

// Prefetch after 75ms hover
<Link href="/users" prefetch>Users</Link>

// Prefetch on mount (useful for near-certain navigations)
<Link href="/dashboard" prefetch="mount">Dashboard</Link>
Enter fullscreen mode Exit fullscreen mode

Inertia v3 Additions

No More Axios

Inertia v3 ships its own built-in XHR client. Axios is no longer a required dependency, saving approximately 15 KB gzipped from your bundle. If you rely on Axios interceptors for token refresh or request logging, add Axios back as a peer dependency — but most projects do not need it.

useHttp for Non-Navigation Requests

Inertia v3 introduces useHttp for HTTP calls that should not trigger page navigation:

import { useHttp } from '@inertiajs/react';

function SubscribeForm() {
    const { post, processing, errors } = useHttp();

    const submit = (e) => {
        e.preventDefault();
        post('/subscribe', { email: e.target.email.value });
    };

    return (
        <form onSubmit={submit}>
            <input name="email" type="email" />
            {errors.email && <p>{errors.email}</p>}
            <button disabled={processing}>Subscribe</button>
        </form>
    );
}
Enter fullscreen mode Exit fullscreen mode

Optimistic Updates

import { router } from '@inertiajs/react';

function toggleTodo(id) {
    router.patch(`/todos/${id}`, { completed: true }, {
        optimistic: (page) => {
            page.props.todos = page.props.todos.map(t =>
                t.id === id ? { ...t, completed: true } : t
            );
        },
    });
    // If the server returns a non-2xx response, the optimistic change is automatically rolled back
}
Enter fullscreen mode Exit fullscreen mode

ESM-Only Output

Inertia v3 packages are ESM only. CommonJS require('@inertiajs/react') will break. Ensure your Vite config does not force CJS output.


CSRF and Auth

Inertia handles CSRF transparently when used with Laravel. The HandleInertiaRequests middleware automatically shares the CSRF token, and Inertia's client attaches it to every request. You do not need to manually include _token in form submissions.

A token mismatch produces a 419 response. Common cause: the session has expired between page load and form submission. Handle this in the Inertia response handler by redirecting to a re-login page.


Common Mistakes

Passing Eloquent models with too many fields. Every prop you pass is serialized to JSON and embedded in the page. Use API resources or ->only() to limit what leaves the server:

// Avoid
'product' => $product,

// Prefer
'product' => $product->only('id', 'name', 'price', 'slug'),
Enter fullscreen mode Exit fullscreen mode

Choosing Inertia for CRUD-heavy admin panels without React/Vue expertise. Inertia adds a JavaScript build step, component structure, and client-side state management overhead. If your team is PHP-native and the UI is primarily forms and tables, Livewire 4 will ship faster with less complexity.

Forgetting that Inertia v3 requires Laravel 11. Upgrading the npm packages without checking the server adapter version will break the app silently.

Using router.visit() for everything. Inertia's router.visit() triggers a full Inertia page visit including scroll reset and component remount. For in-page data mutations (toggle, delete, update), use router.patch(), router.put(), or router.delete() — these preserve the current page component and merge updated props.

No shared data caching. The HandleInertiaRequests middleware share() method runs on every request. Avoid expensive queries there. Cache shared data (authenticated user, permissions, navigation items) in the session or a short-lived cache.


When Inertia Makes Sense (and When It Does Not)

Use Inertia when Prefer Livewire when
Your team knows React or Vue Your team is primarily PHP
You need complex client-side state The UI is mostly forms and lists
You want the full npm ecosystem You want minimal JS tooling
You have rich data visualizations You are building an admin panel
You need SSR for public pages Server-round-trip latency is acceptable

Inertia is the "modern monolith" pattern: you keep Laravel's routing, auth, and data layer, but your frontend developers work in React or Vue with full HMR, TypeScript support, and component tooling — without maintaining a separate SPA deployment.


Verifying the Setup

After scaffolding, confirm Inertia is wiring correctly:

# Run the Laravel dev server
php artisan serve

# In a second terminal, run Vite
npm run dev
Enter fullscreen mode Exit fullscreen mode

Open the browser DevTools Network tab. Navigate between pages using <Link> components. Subsequent navigations should show XHR requests returning application/json with a X-Inertia: true response header — not full HTML. If you see full HTML on every navigation, the HandleInertiaRequests middleware is not registered.

For automated testing, use Inertia's test helpers:

use Inertia\Testing\AssertableInertia as Assert;

test('products index renders correctly', function () {
    $this->get('/products')
        ->assertInertia(fn (Assert $page) => $page
            ->component('Products/Index')
            ->has('products.data', 20)
        );
});
Enter fullscreen mode Exit fullscreen mode

If you need Laravel development in Mumbai, Mumbai Web Designer builds production-grade Laravel applications.

Top comments (0)