{"id":103,"date":"2026-07-20T14:15:00","date_gmt":"2026-07-20T08:45:00","guid":{"rendered":"https:\/\/softcrony.com\/blog\/?p=103"},"modified":"2026-07-20T14:15:00","modified_gmt":"2026-07-20T08:45:00","slug":"inertia-js-v2-laravel-13-fullstack","status":"publish","type":"post","link":"https:\/\/softcrony.com\/blog\/inertia-js-v2-laravel-13-fullstack\/","title":{"rendered":"Inertia.js v2 + Laravel 13: Build Full-Stack Apps Without a Separate API"},"content":{"rendered":"<p>Inertia.js solves one of the most common Laravel architecture questions: &#8220;Should I build a REST API and a separate React\/Vue frontend, or use Blade?&#8221;<\/p>\n<p>The answer in 2026 is neither. Inertia.js gives you a single Laravel application that renders React or Vue components server-driven \u2014 without building an API, without duplicating authentication, without CORS configuration.<\/p>\n<h2>What Is Inertia.js?<\/h2>\n<p>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 \u2014 without a full page reload.<\/p>\n<p>From the developer&#8217;s perspective:<\/p>\n<ul>\n<li>Write Laravel controllers that return Inertia responses instead of API JSON<\/li>\n<li>Write React\/Vue components that receive props from the controller<\/li>\n<li>No REST API. No JWT tokens. No CORS. No separate authentication system.<\/li>\n<\/ul>\n<h2>What&#8217;s New in Inertia.js v2<\/h2>\n<ul>\n<li><strong>Async components<\/strong> \u2014 lazy-load page components for faster initial load<\/li>\n<li><strong>Deferred props<\/strong> \u2014 load expensive data after the page renders<\/li>\n<li><strong>Polling<\/strong> \u2014 built-in data polling without custom useEffect<\/li>\n<li><strong>Infinite scrolling<\/strong> \u2014 first-party infinite scroll support<\/li>\n<li><strong>Prefetching<\/strong> \u2014 preload pages on hover before click<\/li>\n<li><strong>WhenVisible<\/strong> \u2014 load data only when component is in viewport<\/li>\n<li><strong>Merging props<\/strong> \u2014 merge new data into existing props without full reload<\/li>\n<\/ul>\n<h2>Step 1 \u2014 Installation<\/h2>\n<pre><code>composer require inertiajs\/inertia-laravel\r\nnpm install @inertiajs\/react react react-dom\r\nnpm install --save-dev @vitejs\/plugin-react<\/code><\/pre>\n<pre><code>\/\/ vite.config.js\r\nimport { defineConfig } from 'vite';\r\nimport laravel from 'laravel-vite-plugin';\r\nimport react from '@vitejs\/plugin-react';\r\n\r\nexport default defineConfig({\r\n    plugins: [\r\n        laravel({ input: ['resources\/js\/app.jsx'] }),\r\n        react(),\r\n    ],\r\n});<\/code><\/pre>\n<pre><code>\/\/ resources\/views\/app.blade.php\r\n&lt;!DOCTYPE html&gt;\r\n&lt;html&gt;\r\n&lt;head&gt;\r\n    &lt;meta charset=\"utf-8\" \/&gt;\r\n    &lt;meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" \/&gt;\r\n    @viteReactRefresh\r\n    @vite('resources\/js\/app.jsx')\r\n    @inertiaHead\r\n&lt;\/head&gt;\r\n&lt;body&gt;\r\n    @inertia\r\n&lt;\/body&gt;\r\n&lt;\/html&gt;<\/code><\/pre>\n<pre><code>\/\/ resources\/js\/app.jsx\r\nimport { createInertiaApp } from '@inertiajs\/react';\r\nimport { createRoot } from 'react-dom\/client';\r\n\r\ncreateInertiaApp({\r\n    \/\/ Async components \u2014 new in Inertia v2\r\n    resolve: name =&gt; {\r\n        const pages = import.meta.glob('.\/Pages\/**\/*.jsx');\r\n        return pages[`.\/Pages\/${name}.jsx`]();\r\n    },\r\n    setup({ el, App, props }) {\r\n        createRoot(el).render(&lt;App {...props} \/&gt;);\r\n    },\r\n});<\/code><\/pre>\n<h2>Step 2 \u2014 Basic Controller and Page<\/h2>\n<pre><code>&lt;?php\r\n\r\n\/\/ app\/Http\/Controllers\/DashboardController.php\r\nuse Inertia\\Inertia;\r\nuse Inertia\\Response;\r\n\r\nclass DashboardController extends Controller\r\n{\r\n    public function index(): Response\r\n    {\r\n        return Inertia::render('Dashboard', [\r\n            'stats' =&gt; [\r\n                'projects' =&gt; Project::count(),\r\n                'tasks'    =&gt; Task::where('status', 'pending')-&gt;count(),\r\n                'users'    =&gt; User::count(),\r\n            ],\r\n            'recentProjects' =&gt; Project::latest()-&gt;take(5)-&gt;get(),\r\n        ]);\r\n    }\r\n}<\/code><\/pre>\n<pre><code>\/\/ resources\/js\/Pages\/Dashboard.jsx\r\nimport { Head, Link } from '@inertiajs\/react';\r\nimport AppLayout from '@\/Layouts\/AppLayout';\r\n\r\nexport default function Dashboard({ stats, recentProjects }) {\r\n    return (\r\n        &lt;AppLayout&gt;\r\n            &lt;Head title=\"Dashboard\" \/&gt;\r\n\r\n            &lt;div className=\"grid grid-cols-3 gap-6\"&gt;\r\n                &lt;div className=\"bg-white p-6 rounded-xl shadow\"&gt;\r\n                    &lt;p className=\"text-3xl font-bold\"&gt;{stats.projects}&lt;\/p&gt;\r\n                    &lt;p className=\"text-gray-500\"&gt;Projects&lt;\/p&gt;\r\n                &lt;\/div&gt;\r\n                &lt;div className=\"bg-white p-6 rounded-xl shadow\"&gt;\r\n                    &lt;p className=\"text-3xl font-bold\"&gt;{stats.tasks}&lt;\/p&gt;\r\n                    &lt;p className=\"text-gray-500\"&gt;Pending Tasks&lt;\/p&gt;\r\n                &lt;\/div&gt;\r\n            &lt;\/div&gt;\r\n\r\n            &lt;div className=\"mt-8\"&gt;\r\n                &lt;h2 className=\"text-xl font-bold mb-4\"&gt;Recent Projects&lt;\/h2&gt;\r\n                {recentProjects.map(project =&gt; (\r\n                    &lt;Link key={project.id} href={`\/projects\/${project.id}`}&gt;\r\n                        &lt;div className=\"p-4 border rounded-lg mb-2\"&gt;\r\n                            {project.name}\r\n                        &lt;\/div&gt;\r\n                    &lt;\/Link&gt;\r\n                ))}\r\n            &lt;\/div&gt;\r\n        &lt;\/AppLayout&gt;\r\n    );\r\n}<\/code><\/pre>\n<h2>Step 3 \u2014 Deferred Props (New in v2)<\/h2>\n<p>Load expensive data after the page renders \u2014 users see the page immediately while heavy data loads in the background:<\/p>\n<pre><code>&lt;?php\r\n\r\n\/\/ Controller \u2014 defer expensive queries\r\nclass ProjectController extends Controller\r\n{\r\n    public function show(Project $project): Response\r\n    {\r\n        return Inertia::render('Projects\/Show', [\r\n            \/\/ Loads immediately\r\n            'project' =&gt; $project,\r\n\r\n            \/\/ Loads after page renders (deferred)\r\n            'analytics' =&gt; Inertia::defer(fn() =&gt; [\r\n                'views'       =&gt; $project-&gt;analytics()-&gt;count(),\r\n                'completions' =&gt; $project-&gt;tasks()-&gt;where('status', 'completed')-&gt;count(),\r\n                'timeline'    =&gt; $project-&gt;activityLog()-&gt;take(30)-&gt;get(),\r\n            ]),\r\n        ]);\r\n    }\r\n}<\/code><\/pre>\n<pre><code>\/\/ React component \u2014 handle deferred prop\r\nimport { Deferred } from '@inertiajs\/react';\r\n\r\nexport default function ProjectShow({ project, analytics }) {\r\n    return (\r\n        &lt;div&gt;\r\n            &lt;h1&gt;{project.name}&lt;\/h1&gt;\r\n\r\n            &lt;Deferred data=\"analytics\" fallback={&lt;div&gt;Loading analytics...&lt;\/div&gt;}&gt;\r\n                &lt;div className=\"grid grid-cols-3 gap-4\"&gt;\r\n                    &lt;div&gt;Views: {analytics?.views}&lt;\/div&gt;\r\n                    &lt;div&gt;Completed: {analytics?.completions}&lt;\/div&gt;\r\n                &lt;\/div&gt;\r\n            &lt;\/Deferred&gt;\r\n        &lt;\/div&gt;\r\n    );\r\n}<\/code><\/pre>\n<h2>Step 4 \u2014 Polling (New in v2)<\/h2>\n<p>Keep data fresh with automatic polling \u2014 no useEffect or setInterval needed:<\/p>\n<pre><code>import { usePoll } from '@inertiajs\/react';\r\n\r\nexport default function TaskQueue({ pendingTasks }) {\r\n    \/\/ Automatically refetch this page every 5 seconds\r\n    usePoll(5000);\r\n\r\n    return (\r\n        &lt;div&gt;\r\n            &lt;h2&gt;Pending Tasks ({pendingTasks.length})&lt;\/h2&gt;\r\n            {pendingTasks.map(task =&gt; (\r\n                &lt;div key={task.id}&gt;{task.title}&lt;\/div&gt;\r\n            ))}\r\n        &lt;\/div&gt;\r\n    );\r\n}<\/code><\/pre>\n<h2>Step 5 \u2014 Infinite Scrolling (New in v2)<\/h2>\n<pre><code>&lt;?php\r\n\r\n\/\/ Controller \u2014 return paginated data\r\nclass PostController extends Controller\r\n{\r\n    public function index(): Response\r\n    {\r\n        return Inertia::render('Posts\/Index', [\r\n            'posts' =&gt; Post::latest()-&gt;paginate(20),\r\n        ]);\r\n    }\r\n}<\/code><\/pre>\n<pre><code>\/\/ React component \u2014 infinite scroll\r\nimport { useInfiniteScroll } from '@inertiajs\/react';\r\n\r\nexport default function PostsIndex({ posts }) {\r\n    const { data, loadMore, hasMore, loading } = useInfiniteScroll(posts);\r\n\r\n    return (\r\n        &lt;div&gt;\r\n            {data.map(post =&gt; (\r\n                &lt;div key={post.id} className=\"p-4 border-b\"&gt;\r\n                    &lt;h3&gt;{post.title}&lt;\/h3&gt;\r\n                    &lt;p&gt;{post.excerpt}&lt;\/p&gt;\r\n                &lt;\/div&gt;\r\n            ))}\r\n\r\n            {hasMore &amp;&amp; (\r\n                &lt;button onClick={loadMore} disabled={loading}&gt;\r\n                    {loading ? 'Loading...' : 'Load More'}\r\n                &lt;\/button&gt;\r\n            )}\r\n        &lt;\/div&gt;\r\n    );\r\n}<\/code><\/pre>\n<h2>Step 6 \u2014 Forms with Inertia<\/h2>\n<pre><code>import { useForm } from '@inertiajs\/react';\r\n\r\nexport default function CreateProject() {\r\n    const { data, setData, post, processing, errors, reset } = useForm({\r\n        name:        '',\r\n        description: '',\r\n        status:      'active',\r\n    });\r\n\r\n    function handleSubmit(e) {\r\n        e.preventDefault();\r\n        post('\/projects', {\r\n            onSuccess: () =&gt; reset(),\r\n        });\r\n    }\r\n\r\n    return (\r\n        &lt;form onSubmit={handleSubmit}&gt;\r\n            &lt;div&gt;\r\n                &lt;label&gt;Project Name&lt;\/label&gt;\r\n                &lt;input\r\n                    value={data.name}\r\n                    onChange={e =&gt; setData('name', e.target.value)}\r\n                    className={errors.name ? 'border-red-500' : ''}\r\n                \/&gt;\r\n                {errors.name &amp;&amp; &lt;p className=\"text-red-500\"&gt;{errors.name}&lt;\/p&gt;}\r\n            &lt;\/div&gt;\r\n\r\n            &lt;button type=\"submit\" disabled={processing}&gt;\r\n                {processing ? 'Creating...' : 'Create Project'}\r\n            &lt;\/button&gt;\r\n        &lt;\/form&gt;\r\n    );\r\n}<\/code><\/pre>\n<h2>Step 7 \u2014 Prefetching (New in v2)<\/h2>\n<pre><code>import { Link } from '@inertiajs\/react';\r\n\r\n\/\/ Prefetch on hover \u2014 page loads before user clicks\r\nexport default function ProjectList({ projects }) {\r\n    return (\r\n        &lt;ul&gt;\r\n            {projects.map(project =&gt; (\r\n                &lt;li key={project.id}&gt;\r\n                    &lt;Link\r\n                        href={`\/projects\/${project.id}`}\r\n                        prefetch  \/\/ Preloads on hover\r\n                    &gt;\r\n                        {project.name}\r\n                    &lt;\/Link&gt;\r\n                &lt;\/li&gt;\r\n            ))}\r\n        &lt;\/ul&gt;\r\n    );\r\n}<\/code><\/pre>\n<h2>When to Use Inertia vs REST API<\/h2>\n<table>\n<thead>\n<tr>\n<th>Use Inertia when<\/th>\n<th>Use REST API when<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Single web application<\/td>\n<td>Mobile app + web app sharing data<\/td>\n<\/tr>\n<tr>\n<td>Internal tools and dashboards<\/td>\n<td>Third-party integrations need the API<\/td>\n<\/tr>\n<tr>\n<td>CMS, admin panels, SaaS apps<\/td>\n<td>Public API for developers<\/td>\n<\/tr>\n<tr>\n<td>Small to medium team<\/td>\n<td>Separate frontend and backend teams<\/td>\n<\/tr>\n<tr>\n<td>Fast initial delivery<\/td>\n<td>Complex real-time features (WebSockets)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>If you&#8217;re building a new Laravel application and want the fastest path to a full-stack product without the overhead of a separate API, <a href=\"https:\/\/softcrony.com\/contact\/\">our team at Softcrony builds with Inertia.js daily<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Inertia.js solves one of the most common Laravel architecture questions: &#8220;Should I build a REST API and a separate React\/Vue frontend, or use Blade?&#8221; The answer in 2026 is neither. Inertia.js gives you a single Laravel application that renders React or Vue components server-driven \u2014 without building an API, without duplicating authentication, without CORS configuration. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":105,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[104,103,19,30,48,105],"class_list":["post-103","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-full-stack","tag-inertia-js","tag-laravel","tag-php","tag-react","tag-spa"],"_links":{"self":[{"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/posts\/103","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/comments?post=103"}],"version-history":[{"count":2,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/posts\/103\/revisions"}],"predecessor-version":[{"id":106,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/posts\/103\/revisions\/106"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/media\/105"}],"wp:attachment":[{"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/media?parent=103"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/categories?post=103"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/tags?post=103"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}