Fix: Inertia.js Not Working — Shared Data, Lazy Props, Versioning, Forms, and SSR
Quick Answer
How to fix Inertia.js errors — Inertia.render not returning a component, shared data missing on every page, lazy props not deferring, asset versioning forcing reloads, useForm helper, and SSR setup.
The Error
You set up Inertia + Laravel + React but the page renders blank:
// PostController.php
public function index() {
return Inertia::render('Posts/Index', ['posts' => Post::all()]);
}Browser console:
TypeError: Cannot read properties of undefined (reading 'name')Or usePage().props.user is undefined despite being set in middleware:
const user = usePage().props.user;
console.log(user); // undefinedOr every navigation triggers a full page reload:
[network] Initial load: 200 OK (text/html)
[click link] Full reload: 200 OK (text/html)
# Should be partial JSON response.Or useForm() posts the form but the page doesn’t update:
const form = useForm({ title: "" });
form.post("/posts"); // Server responds, but UI doesn't refresh.Why This Happens
Inertia.js is a “modern monolith” bridge. Your backend (Laravel/Rails/etc.) returns a JSON payload describing which component and props to render; the Inertia client-side library swaps in the component without a full reload.
Three common pain points:
- Two halves to set up. Server adapter (Laravel:
inertia/inertia-laravel; Rails:inertia-rails) and client adapter (@inertiajs/react,@inertiajs/vue3, etc.). Either side misconfigured breaks the SPA experience. - Shared data needs middleware. Cross-page props (current user, flash messages) come from
HandleInertiaRequestsmiddleware. Without it (or with wrong shape), props are missing. - Asset versioning. When you deploy new JS, in-flight Inertia visits should reload. Inertia compares an
X-Inertia-Versionheader — if your server’s version changes, the client does a full reload to fetch new assets. - Forms via
useFormare reactive. Submitting via plainfetchworks but doesn’t update the page;useForm.postdoes an Inertia request that swaps props.
Fix 1: Server-Side Adapter Setup (Laravel)
composer require inertiajs/inertia-laravelapp/Http/Middleware/HandleInertiaRequests.php:
<?php
namespace App\Http\Middleware;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
protected $rootView = 'app';
public function version(Request $request): ?string
{
return parent::version($request);
}
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'auth' => [
'user' => fn () => $request->user()?->only('id', 'name', 'email'),
],
'flash' => [
'success' => fn () => $request->session()->get('success'),
'error' => fn () => $request->session()->get('error'),
],
]);
}
}Register in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\App\Http\Middleware\HandleInertiaRequests::class,
]);
})Your root view (resources/views/app.blade.php):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@viteReactRefresh
@vite('resources/js/app.tsx')
@inertiaHead
</head>
<body>
@inertia
</body>
</html>@inertia is the directive that renders the initial Inertia response into an HTML element with id="app". @inertiaHead is for SSR head tags.
Pro Tip: For Rails, the equivalent gem is inertia-rails. The patterns differ but the concept is the same: middleware shares data, controllers render inertia: 'Component', props: {...}.
Fix 2: Client-Side Setup (React)
npm install @inertiajs/reactresources/js/app.tsx:
import { createInertiaApp } from "@inertiajs/react";
import { createRoot } from "react-dom/client";
createInertiaApp({
resolve: (name) => {
const pages = import.meta.glob("./Pages/**/*.tsx");
return pages[`./Pages/${name}.tsx`]();
},
setup({ el, App, props }) {
createRoot(el).render(<App {...props} />);
},
progress: { color: "#4B5563" },
});The resolve function maps Posts/Index (from Inertia::render('Posts/Index', ...)) to a component file. Use glob imports for code splitting:
resolve: (name) => {
const pages = import.meta.glob("./Pages/**/*.tsx", { eager: false });
return pages[`./Pages/${name}.tsx`]();
},eager: false produces dynamic imports — pages load on demand.
Component file:
// resources/js/Pages/Posts/Index.tsx
import { Head, usePage, Link } from "@inertiajs/react";
interface Post {
id: number;
title: string;
}
export default function Index() {
const { posts, auth } = usePage().props as { posts: Post[]; auth: { user: any } };
return (
<>
<Head title="Posts" />
<h1>Posts</h1>
<p>Welcome, {auth.user?.name}</p>
<ul>
{posts.map((p) => (
<li key={p.id}>
<Link href={`/posts/${p.id}`}>{p.title}</Link>
</li>
))}
</ul>
</>
);
}usePage().props is typed as Record<string, unknown> by default. Cast to your shape (or define a global type augmentation).
Common Mistake: Using regular <a href="..."> instead of <Link href="...">. Plain <a> causes a full reload; <Link> does an Inertia visit.
Fix 3: Shared Data Patterns
Some props (current user, flash messages, app version) should appear on every page without manually adding them.
In HandleInertiaRequests::share():
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'auth' => fn () => [
'user' => $request->user()?->only('id', 'name', 'avatar'),
],
'app' => [
'name' => config('app.name'),
'env' => config('app.env'),
],
'flash' => function () use ($request) {
return [
'success' => $request->session()->get('success'),
'error' => $request->session()->get('error'),
];
},
]);
}Three patterns:
- Plain values — same on every request.
- Closures — computed per-request (e.g.
fn () => $request->user()). - Inertia lazy props — see Fix 4.
Access in components:
const { auth, flash } = usePage().props;For TypeScript globally:
// resources/js/types/inertia.d.ts
import "@inertiajs/react";
declare module "@inertiajs/react" {
interface PageProps {
auth: { user: { id: number; name: string } | null };
flash: { success?: string; error?: string };
app: { name: string; env: string };
}
}Now usePage().props.auth is typed across all components.
Common Mistake: Returning expensive data eagerly in share(). Every request runs share — heavy DB queries here slow every page. Use lazy props.
Fix 4: Lazy Props
For data that’s expensive and only needed sometimes:
return Inertia::render('Dashboard', [
'users' => fn () => User::all(), // Eager — runs on every render
'stats' => Inertia::lazy(fn () => Stats::compute()), // Lazy — only on explicit partial reload
]);Inertia::lazy() defers the computation. The client can fetch lazily:
import { router } from "@inertiajs/react";
function StatsPanel() {
const [loading, setLoading] = useState(false);
return (
<button onClick={() => {
router.reload({ only: ['stats'], onStart: () => setLoading(true) });
}}>
Load Stats
</button>
);
}router.reload({ only: ['stats'] }) sends an Inertia partial reload that only refetches stats. The server runs the Inertia::lazy closure and returns just that prop.
For Inertia 2.0+, Inertia::defer() is the new name for the same behavior with extra features (polling, intersection-based loading):
return Inertia::render('Posts/Show', [
'post' => Post::find($id),
'comments' => Inertia::defer(fn () => Comment::where('post_id', $id)->get()),
]);The comments prop loads after the initial page renders — better LCP.
Pro Tip: Use defer (or lazy) for anything below the fold or in a tab that’s not initially visible. Faster initial render, less data per page.
Fix 5: Asset Versioning
When you deploy new JS, Inertia must know to do a full reload (not an in-page swap with mismatched code).
HandleInertiaRequests::version():
public function version(Request $request): ?string
{
return md5_file(public_path('build/manifest.json'));
}This hashes the Vite manifest. Every new build produces a different hash; clients with the old hash get a 409 response from Inertia and reload.
For non-Vite setups:
public function version(Request $request): ?string
{
return parent::version($request); // Defaults to hashing `public/mix-manifest.json` if present
}Or pin to a manual version:
public function version(Request $request): ?string
{
return 'v1.2.3'; // Bump on every deploy
}Common Mistake: Forgetting versioning entirely. Clients hang on old JS bundles indefinitely — no auto-reload. Implement version() early.
Fix 6: Forms With useForm
For form submissions:
import { useForm } from "@inertiajs/react";
function CreatePost() {
const form = useForm({
title: "",
body: "",
});
function submit(e: React.FormEvent) {
e.preventDefault();
form.post("/posts", {
onSuccess: () => form.reset(),
});
}
return (
<form onSubmit={submit}>
<input
value={form.data.title}
onChange={(e) => form.setData("title", e.target.value)}
/>
{form.errors.title && <p>{form.errors.title}</p>}
<textarea
value={form.data.body}
onChange={(e) => form.setData("body", e.target.value)}
/>
{form.errors.body && <p>{form.errors.body}</p>}
<button disabled={form.processing}>
{form.processing ? "Saving..." : "Save"}
</button>
</form>
);
}The form helper:
form.data— current values.form.setData(key, value)— update.form.processing— true during submit.form.errors— validation errors (from Laravel’s$request->validate(...)).form.post(url, options)— submit via Inertia.form.reset(...keys)— reset to initial values.
Errors come from the Laravel controller:
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|min:3',
'body' => 'required',
]);
Post::create($validated);
return redirect()->route('posts.index')->with('success', 'Post created!');
}On validation failure, Laravel automatically returns a 422 with the errors. Inertia maps them into form.errors.
Common Mistake: Using fetch() or axios() to submit. The response isn’t an Inertia response, so the page doesn’t update. Always use form.post, form.put, etc. for Inertia.
Fix 7: Server-Side Rendering
For SEO and faster first paint, set up SSR:
npm install @inertiajs/serverresources/js/ssr.tsx:
import { createInertiaApp } from "@inertiajs/react";
import createServer from "@inertiajs/react/server";
import ReactDOMServer from "react-dom/server";
createServer((page) =>
createInertiaApp({
page,
render: ReactDOMServer.renderToString,
resolve: (name) => {
const pages = import.meta.glob("./Pages/**/*.tsx");
return pages[`./Pages/${name}.tsx`]();
},
setup: ({ App, props }) => <App {...props} />,
}),
);In vite.config.ts:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import laravel from "laravel-vite-plugin";
export default defineConfig({
plugins: [
react(),
laravel({
input: ["resources/js/app.tsx"],
ssr: "resources/js/ssr.tsx",
refresh: true,
}),
],
});Build for SSR:
npm run build
# Produces both client and SSR bundles.Run the SSR server:
php artisan inertia:start-ssrNow Inertia renders pages on the server, hydrates on the client.
For production, run the SSR server alongside your PHP-FPM / web server. PM2 or systemd manage the Node process.
Pro Tip: SSR is a real Node process. Don’t put database queries in your React components — they run server-side and double-execute. Pass everything as Inertia props.
Fix 8: Progress Bar and Visit Lifecycle
The default progress bar:
createInertiaApp({
// ...
progress: { color: "#4B5563", showSpinner: false },
});To disable and use your own:
import { router } from "@inertiajs/react";
router.on("start", (event) => {
showLoadingIndicator();
});
router.on("finish", (event) => {
hideLoadingIndicator();
});For visit options:
router.visit("/posts", {
method: "get",
data: { sort: "newest" },
preserveState: true, // Don't reset component state
preserveScroll: true, // Don't scroll to top
only: ["posts"], // Partial reload
onSuccess: (page) => { ... },
onError: (errors) => { ... },
});For programmatic navigation:
router.get("/posts");
router.post("/posts", { title: "..." });
router.put("/posts/1", { title: "..." });
router.delete("/posts/1");
router.reload(); // Re-fetch current pageStill Not Working?
A few less-obvious failures:
Page component not found. Glob pattern doesn’t match. Verify the path:./Pages/Posts/Index.tsxforInertia::render('Posts/Index', ...).- Initial page loads slow. Eager glob loads all pages on first hit. Use
eager: falsefor lazy/code-split loading. - Errors not showing in form. Laravel returned a JSON error response (not an Inertia response). The 422 status with validation errors should come through as a regular response with
X-Inertia: trueheader; check your middleware order. - CSRF token mismatch. Inertia requests need CSRF protection. The default Laravel setup handles it automatically; verify
VerifyCsrfTokenmiddleware is registered. - Browser back button broken. Inertia maintains its own history. Don’t manipulate
window.historydirectly. - Modals on the same page break Inertia. A “modal route” requires extra setup. Look at the
inertia-modalpackages or implement a Modal component that doesn’t use Inertia visits. - Vite HMR doesn’t trigger.
@viteReactRefreshdirective missing or in the wrong place. Place it before@vite(...). - TypeScript can’t find
usePageprops. AugmentPagePropsinterface (see Fix 3).
For related full-stack and SPA issues, see Laravel queue job not processing, Vite failed to resolve import, React hydration error, and Next.js server action not working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Playwright Component Testing Not Working — Mount Fixture, Vite Config, Styles, and TypeScript
How to fix Playwright component testing errors — playwright-ct.config not found, mount fixture undefined, CSS not loaded in tests, Vite alias for imports, TypeScript paths, hooks (beforeMount), and snapshot strategy.
Fix: next-themes Not Working — Hydration Mismatch, Tailwind Dark Mode, FOUC, and System Preference
How to fix next-themes errors — hydration mismatch on mount, FOUC flash before theme applies, Tailwind dark: classes not switching, ThemeProvider in App Router, defaultTheme system not respected, and TypeScript types.
Fix: PGlite Not Working — IndexedDB Persistence, Worker Setup, Extensions, and Live Queries
How to fix PGlite errors — async init not awaited, IndexedDB persistence lost on reload, Web Worker isolation, pgvector and other extensions, live queries with @electric-sql/pglite-react, and migration patterns.
Fix: React 19 Actions Not Working — useActionState, useFormStatus, useOptimistic, and form action
How to fix React 19 actions errors — useActionState signature, form action vs onSubmit, useFormStatus must be in child, useOptimistic state desync, Server Actions in client components, and error handling.