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
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
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
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>
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} />);
},
});
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);
},
});
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'),
]);
}
}
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>
);
}
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'),
]);
// React component
import { Deferred } from '@inertiajs/react';
<Deferred data="stats" fallback={<p>Loading stats...</p>}>
{({ stats }) => <StatsChart data={stats} />}
</Deferred>
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>
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>
);
}
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
}
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'),
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
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)
);
});
If you need Laravel development in Mumbai, Mumbai Web Designer builds production-grade Laravel applications.
Top comments (0)