Inertia.js v2 + Laravel 13: Build Full-Stack Apps Without a Separate API

calendar_today July 20, 2026
person info@softcrony.com
folder Uncategorized

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