Inertia.js solves one of the most common Laravel architecture questions: “Should I build a REST API and a separate React/Vue frontend, or use Blade?”
The answer in 2026 is neither. Inertia.js gives you a single Laravel application that renders React or Vue components server-driven — without building an API, without duplicating authentication, without CORS configuration.
What Is Inertia.js?
Inertia.js is a protocol and library that connects your Laravel backend directly to your React or Vue frontend. When a user navigates in the browser, Inertia intercepts the request, makes an XHR call to Laravel, receives the new page component and its data as JSON, and renders it client-side — without a full page reload.
From the developer’s perspective:
- Write Laravel controllers that return Inertia responses instead of API JSON
- Write React/Vue components that receive props from the controller
- No REST API. No JWT tokens. No CORS. No separate authentication system.
What’s New in Inertia.js v2
- Async components — lazy-load page components for faster initial load
- Deferred props — load expensive data after the page renders
- Polling — built-in data polling without custom useEffect
- Infinite scrolling — first-party infinite scroll support
- Prefetching — preload pages on hover before click
- WhenVisible — load data only when component is in viewport
- Merging props — merge new data into existing props without full reload
Step 1 — Installation
composer require inertiajs/inertia-laravel
npm install @inertiajs/react react react-dom
npm install --save-dev @vitejs/plugin-react
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
laravel({ input: ['resources/js/app.jsx'] }),
react(),
],
});
// resources/views/app.blade.php
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@viteReactRefresh
@vite('resources/js/app.jsx')
@inertiaHead
</head>
<body>
@inertia
</body>
</html>
// resources/js/app.jsx
import { createInertiaApp } from '@inertiajs/react';
import { createRoot } from 'react-dom/client';
createInertiaApp({
// Async components — new in Inertia v2
resolve: name => {
const pages = import.meta.glob('./Pages/**/*.jsx');
return pages[`./Pages/${name}.jsx`]();
},
setup({ el, App, props }) {
createRoot(el).render(<App {...props} />);
},
});
Step 2 — Basic Controller and Page
<?php
// app/Http/Controllers/DashboardController.php
use Inertia\Inertia;
use Inertia\Response;
class DashboardController extends Controller
{
public function index(): Response
{
return Inertia::render('Dashboard', [
'stats' => [
'projects' => Project::count(),
'tasks' => Task::where('status', 'pending')->count(),
'users' => User::count(),
],
'recentProjects' => Project::latest()->take(5)->get(),
]);
}
}
// resources/js/Pages/Dashboard.jsx
import { Head, Link } from '@inertiajs/react';
import AppLayout from '@/Layouts/AppLayout';
export default function Dashboard({ stats, recentProjects }) {
return (
<AppLayout>
<Head title="Dashboard" />
<div className="grid grid-cols-3 gap-6">
<div className="bg-white p-6 rounded-xl shadow">
<p className="text-3xl font-bold">{stats.projects}</p>
<p className="text-gray-500">Projects</p>
</div>
<div className="bg-white p-6 rounded-xl shadow">
<p className="text-3xl font-bold">{stats.tasks}</p>
<p className="text-gray-500">Pending Tasks</p>
</div>
</div>
<div className="mt-8">
<h2 className="text-xl font-bold mb-4">Recent Projects</h2>
{recentProjects.map(project => (
<Link key={project.id} href={`/projects/${project.id}`}>
<div className="p-4 border rounded-lg mb-2">
{project.name}
</div>
</Link>
))}
</div>
</AppLayout>
);
}
Step 3 — Deferred Props (New in v2)
Load expensive data after the page renders — users see the page immediately while heavy data loads in the background:
<?php
// Controller — defer expensive queries
class ProjectController extends Controller
{
public function show(Project $project): Response
{
return Inertia::render('Projects/Show', [
// Loads immediately
'project' => $project,
// Loads after page renders (deferred)
'analytics' => Inertia::defer(fn() => [
'views' => $project->analytics()->count(),
'completions' => $project->tasks()->where('status', 'completed')->count(),
'timeline' => $project->activityLog()->take(30)->get(),
]),
]);
}
}
// React component — handle deferred prop
import { Deferred } from '@inertiajs/react';
export default function ProjectShow({ project, analytics }) {
return (
<div>
<h1>{project.name}</h1>
<Deferred data="analytics" fallback={<div>Loading analytics...</div>}>
<div className="grid grid-cols-3 gap-4">
<div>Views: {analytics?.views}</div>
<div>Completed: {analytics?.completions}</div>
</div>
</Deferred>
</div>
);
}
Step 4 — Polling (New in v2)
Keep data fresh with automatic polling — no useEffect or setInterval needed:
import { usePoll } from '@inertiajs/react';
export default function TaskQueue({ pendingTasks }) {
// Automatically refetch this page every 5 seconds
usePoll(5000);
return (
<div>
<h2>Pending Tasks ({pendingTasks.length})</h2>
{pendingTasks.map(task => (
<div key={task.id}>{task.title}</div>
))}
</div>
);
}
Step 5 — Infinite Scrolling (New in v2)
<?php
// Controller — return paginated data
class PostController extends Controller
{
public function index(): Response
{
return Inertia::render('Posts/Index', [
'posts' => Post::latest()->paginate(20),
]);
}
}
// React component — infinite scroll
import { useInfiniteScroll } from '@inertiajs/react';
export default function PostsIndex({ posts }) {
const { data, loadMore, hasMore, loading } = useInfiniteScroll(posts);
return (
<div>
{data.map(post => (
<div key={post.id} className="p-4 border-b">
<h3>{post.title}</h3>
<p>{post.excerpt}</p>
</div>
))}
{hasMore && (
<button onClick={loadMore} disabled={loading}>
{loading ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
}
Step 6 — Forms with Inertia
import { useForm } from '@inertiajs/react';
export default function CreateProject() {
const { data, setData, post, processing, errors, reset } = useForm({
name: '',
description: '',
status: 'active',
});
function handleSubmit(e) {
e.preventDefault();
post('/projects', {
onSuccess: () => reset(),
});
}
return (
<form onSubmit={handleSubmit}>
<div>
<label>Project Name</label>
<input
value={data.name}
onChange={e => setData('name', e.target.value)}
className={errors.name ? 'border-red-500' : ''}
/>
{errors.name && <p className="text-red-500">{errors.name}</p>}
</div>
<button type="submit" disabled={processing}>
{processing ? 'Creating...' : 'Create Project'}
</button>
</form>
);
}
Step 7 — Prefetching (New in v2)
import { Link } from '@inertiajs/react';
// Prefetch on hover — page loads before user clicks
export default function ProjectList({ projects }) {
return (
<ul>
{projects.map(project => (
<li key={project.id}>
<Link
href={`/projects/${project.id}`}
prefetch // Preloads on hover
>
{project.name}
</Link>
</li>
))}
</ul>
);
}
When to Use Inertia vs REST API
| Use Inertia when | Use REST API when |
|---|---|
| Single web application | Mobile app + web app sharing data |
| Internal tools and dashboards | Third-party integrations need the API |
| CMS, admin panels, SaaS apps | Public API for developers |
| Small to medium team | Separate frontend and backend teams |
| Fast initial delivery | Complex real-time features (WebSockets) |
If you’re building a new Laravel application and want the fastest path to a full-stack product without the overhead of a separate API, our team at Softcrony builds with Inertia.js daily.
Leave a comment