How a complete architectural rewrite is bringing React developers into the Laravel ecosystem—and why it might be the full-stack solution you've been searching for.
If you're a React developer who's ever built a full-stack application, you know the pain. You've probably cobbled together Next.js with API routes, wrestled with server actions, debugged hydration mismatches, and wondered why building a simple CRUD app requires understanding the intricacies of Server Components, edge functions, and ten different rendering strategies.
What if I told you there's a better way? One that lets you keep writing React the framework you know and love while eliminating 90% of the complexity? Enter Inertia.js.
Let me be direct: the modern JavaScript ecosystem has sold React developers a dream that's turned into a nightmare.
We were promised that JavaScript everywhere would simplify our lives. Instead, we got:
API Route Chaos: In Next.js, managing API routes at scale becomes unwieldy fast. As one developer put it, "improper structuring of API routes can lead to performance bottlenecks, code duplication, and security vulnerabilities." You end up with a /pages/api folder that looks like someone threw spaghetti at a wall.
The Complexity Tax: The App Router in Next.js introduced powerful features—Server Components, Layouts, sophisticated caching—but at what cost? Developers are no longer asking "Why doesn't this work?" They're asking "Why does this work this way?" The cognitive overhead is real, especially for teams building B2B SaaS applications where shipping features fast matters more than bleeding-edge optimizations.
The Separation Problem: Even with API routes, you're still mentally context-switching between frontend and backend concerns. You write a POST handler in /api/users, then flip to your component to call it with fetch or axios, manage loading states, handle errors, deal with CORS in development... it's exhausting.
Taylor Otwell, creator of Laravel, put it perfectly in a recent discussion:
"I don't see a full-stack story in JavaScript yet that would allow me to realistically sit down and build something like Forge or Vapor from start to finish. Maybe I'm missing it. The MVP start-ups I do see fully built on current JS meta frameworks are much thinner. The stereotypical API call to an AI service. Not much meat on the bones."
He's not wrong. Rails and Laravel were built with the express purpose of allowing a single developer to build the next GitHub, Airbnb, or Shopify prototyped from beginning to end. That's the promise React developers are still chasing.
Here's the radical idea behind Inertia: What if you didn't need an API?
Inertia.js allows you to create fully client-side (or server-side) rendered, single-page React apps without the complexity that comes with modern SPAs. It does this by leveraging existing server-side patterns that work.
The concept is beautifully simple:
Think of Inertia as glue between your server-side framework and your client-side framework. It's not a replacement for either—it makes them work together seamlessly.
In August 2024, Taylor Otwell announced Inertia 2.0 at Laracon US. The stable release followed in early 2025, and it's a game-changer. The core library has been completely rewritten to architecturally support asynchronous requests, unlocking an entirely new set of features that make building modern SPAs feel... easy.
Previously, all Inertia requests were synchronous. If you navigated to a new page or refreshed a prop, that request would block others. No more.
Inertia 2.0 introduces full asynchronous support. Multiple requests can happen simultaneously without canceling each other. This fundamental architectural change enables everything else in this release.
For example, the existing reload method is now async by default:
1// Non-blocking, doesn't cancel other requests2// Disables progress indicator by default3router.reload({ only: ['users'] })
This might seem subtle, but it's transformative. You can now build interfaces that feel genuinely responsive loading different parts of the page independently, polling for updates, lazy loading data—all without the complexity of managing WebSocket connections or elaborate client-side state machines.
One of the most impressive features in 2.0 is prefetching. Inertia can now load pages in the background before users navigate to them, dramatically improving perceived performance.
By default, Inertia prefetches data when a user hovers over a link for more than 75ms. When they click, the page appears instantly because the data is already loaded and cached.
1import { Link } from '@inertiajs/react'2 3// Automatic prefetching on hover4<Link href="/users/123">View Profile</Link>5 6// Or prefetch immediately on page load7<Link href="/dashboard" prefetch>Dashboard</Link>
The cache is smart too data is cached for 30 seconds by default, and you can manually control the TTL. Even if cache expires, the page loads instantly with stale data while Inertia fetches fresh data in the background.
This is the kind of optimization that would require significant engineering effort in a traditional React + API architecture. With Inertia 2.0, it's a single prop.
Here's a common pattern: you have a dashboard page that shows user info (fast) and recent analytics (slow because it queries a data warehouse). In a traditional setup, the entire page waits for the slowest query.
Inertia 2.0 introduces deferred props—data that loads in the background after the initial page render:
Laravel Controller:
1use Inertia\Inertia;2 3public function show()4{5 return Inertia::render('Dashboard', [6 'user' => Auth::user(), // Loaded immediately7 'analytics' => Inertia::defer(fn () => $this->getAnalytics()), // Loaded async8 ]);9}
React Component:
1export default function Dashboard({ user, analytics }) { 2 return ( 3 <div> 4 <h1>Welcome back, {user.name}!</h1> 5 6 {analytics ? ( 7 <AnalyticsChart data={analytics} /> 8 ) : ( 9 <LoadingSpinner />10 )}11 </div>12 )13}
The page renders immediately with the user data. A split second later, the analytics appear. The user sees a fast page load, and you don't need to orchestrate multiple API calls or manage complex loading states.
Building on deferred props, Inertia 2.0 introduces the WhenVisible component. It loads data only when elements scroll into view perfect for optimizing pages with lots of content.
1import { WhenVisible } from '@inertiajs/react'2 3<WhenVisible data="comments" fallback={<LoadingSkeleton />}>4 {({ comments }) => (5 <CommentSection comments={comments} />6 )}7</WhenVisible>
On the backend, you defer the prop:
1return Inertia::render('Post/Show', [2 'post' => $post,3 'comments' => Inertia::defer(fn () => $post->comments),4]);
The comments only load when the user scrolls down to that section. This drastically reduces initial page load time and database queries for content users might never see.
Infinite scrolling used to require libraries, pagination logic, scroll event handlers, and careful state management. Not anymore.
1import { InfiniteScroll } from '@inertiajs/react' 2 3export default function Feed({ posts }) { 4 return ( 5 <div> 6 {posts.data.map(post => <PostCard key={post.id} post={post} />)} 7 8 <InfiniteScroll 9 loadMore={() => router.reload({10 only: ['posts'],11 data: { page: posts.current_page + 1 }12 })}13 hasMore={posts.current_page < posts.last_page}14 />15 </div>16 )17}
On the backend, use Inertia::merge():
1public function index(Request $request)2{3 return Inertia::render('Feed', [4 'posts' => Inertia::merge(5 Post::latest()->paginate(10)6 ),7 ]);8}
Inertia handles merging new results with existing data automatically. You get smooth, infinite scrolling with just a few lines of code.
Need real-time updates without the complexity of WebSockets? Polling is now trivial:
1import { useEffect } from 'react' 2import { router } from '@inertiajs/react' 3 4export default function Leaderboard({ scores }) { 5 useEffect(() => { 6 const interval = setInterval(() => { 7 router.reload({ only: ['scores'] }) 8 }, 5000) // Poll every 5 seconds 9 10 return () => clearInterval(interval)11 }, [])12 13 return (14 <div>15 {scores.map(score => (16 <div key={score.id}>{score.player}: {score.points}</div>17 ))}18 </div>19 )20}
For many use cases dashboards, leaderboards, status pages this is simpler and more reliable than maintaining WebSocket connections.
One often overlooked concern: when users log out, their browsing history still contains sensitive data in the browser's history state. Inertia 2.0 solves this with history encryption, enabled by default.
Session state is encrypted automatically. When a user logs out, privileged information is cleared from the browser history. This happens without any configuration—it just works.
If you've built forms in React, you know it's tedious. You manage state, handle submissions, deal with validation errors, show loading states, handle file uploads... it's a lot of boilerplate.
Inertia's useForm hook eliminates most of it:
1import { useForm } from '@inertiajs/react' 2 3export default function CreateUser() { 4 const { data, setData, post, processing, errors } = useForm({ 5 name: '', 6 email: '', 7 password: '', 8 }) 9 10 function submit(e) {11 e.preventDefault()12 post('/users')13 }14 15 return (16 <form onSubmit={submit}>17 <input18 type="text"19 value={data.name}20 onChange={e => setData('name', e.target.value)}21 />22 {errors.name && <div className="error">{errors.name}</div>}23 24 <input25 type="email"26 value={data.email}27 onChange={e => setData('email', e.target.value)}28 />29 {errors.email && <div className="error">{errors.email}</div>}30 31 <input32 type="password"33 value={data.password}34 onChange={e => setData('password', e.target.value)}35 />36 {errors.password && <div className="error">{errors.password}</div>}37 38 <button type="submit" disabled={processing}>39 {processing ? 'Creating...' : 'Create User'}40 </button>41 </form>42 )43}
On the Laravel backend:
1public function store(Request $request) 2{ 3 $request->validate([ 4 'name' => ['required', 'max:50'], 5 'email' => ['required', 'email', 'unique:users'], 6 'password' => ['required', 'min:8'], 7 ]); 8 9 User::create($request->all());10 11 return redirect()->route('users.index');12}
Notice what you don't have to do:
processing booleanThe validation errors from Laravel automatically flow back to the frontend. The form knows when it's processing. File uploads work automatically—Inertia converts them to FormData behind the scenes.
The useForm hook provides several quality-of-life features:
Progress tracking for file uploads:
1const { data, setData, post, progress } = useForm({ avatar: null })2 3<input type="file" onChange={e => setData('avatar', e.target.files[0])} />4{progress && <ProgressBar value={progress.percentage} />}
Dirty checking:
1const { data, isDirty, reset } = useForm({ name: 'John' })2 3{isDirty && <button onClick={reset}>Reset Changes</button>}
Success states:
1const { wasSuccessful, recentlySuccessful } = useForm({ ... })2 3{recentlySuccessful && <div className="success">Saved!</div>}
Transform data before submission:
1const form = useForm({ name: '', email: '' })2 3form.transform((data) => ({4 ...data,5 name: data.name.trim(),6}))7 8form.post('/users')
Let me show you two approaches to building the same feature: a user profile page with the ability to update information.
API Route (/app/api/users/[id]/route.ts):
1import { NextRequest } from 'next/server' 2import { db } from '@/lib/db' 3 4export async function GET( 5 request: NextRequest, 6 { params }: { params: { id: string } } 7) { 8 const user = await db.user.findUnique({ 9 where: { id: params.id }10 })11 12 if (!user) {13 return Response.json({ error: 'Not found' }, { status: 404 })14 }15 16 return Response.json(user)17}18 19export async function PATCH(20 request: NextRequest,21 { params }: { params: { id: string } }22) {23 const body = await request.json()24 25 // Manual validation26 if (!body.name || body.name.length > 50) {27 return Response.json(28 { errors: { name: 'Name is required and must be less than 50 characters' } },29 { status: 422 }30 )31 }32 33 const user = await db.user.update({34 where: { id: params.id },35 data: body36 })37 38 return Response.json(user)39}
React Component (/app/users/[id]/edit/page.tsx):
1'use client' 2import { useEffect, useState } from 'react' 3import { useRouter, useParams } from 'next/navigation' 4 5export default function EditUser() { 6 const router = useRouter() 7 const params = useParams() 8 const [user, setUser] = useState(null) 9 const [loading, setLoading] = useState(true)10 const [submitting, setSubmitting] = useState(false)11 const [errors, setErrors] = useState({})12 const [formData, setFormData] = useState({13 name: '',14 email: '',15 })16 17 useEffect(() => {18 fetch(`/api/users/${params.id}`)19 .then(res => res.json())20 .then(data => {21 setUser(data)22 setFormData({ name: data.name, email: data.email })23 setLoading(false)24 })25 }, [params.id])26 27 async function handleSubmit(e) {28 e.preventDefault()29 setSubmitting(true)30 setErrors({})31 32 const res = await fetch(`/api/users/${params.id}`, {33 method: 'PATCH',34 headers: { 'Content-Type': 'application/json' },35 body: JSON.stringify(formData)36 })37 38 const data = await res.json()39 40 if (res.ok) {41 router.push('/users')42 } else {43 setErrors(data.errors || {})44 setSubmitting(false)45 }46 }47 48 if (loading) return <div>Loading...</div>49 50 return (51 <form onSubmit={handleSubmit}>52 <input53 type="text"54 value={formData.name}55 onChange={e => setFormData({ ...formData, name: e.target.value })}56 />57 {errors.name && <div>{errors.name}</div>}58 59 <button disabled={submitting}>60 {submitting ? 'Saving...' : 'Save'}61 </button>62 </form>63 )64}
That's a lot of code. And we haven't even covered:
Laravel Controller:
1public function edit(User $user) 2{ 3 return Inertia::render('Users/Edit', [ 4 'user' => $user, 5 ]); 6} 7 8public function update(Request $request, User $user) 9{10 $user->update($request->validate([11 'name' => ['required', 'max:50'],12 'email' => ['required', 'email'],13 ]));14 15 return redirect()->route('users.index');16}
React Component:
1import { useForm } from '@inertiajs/react' 2 3export default function Edit({ user }) { 4 const { data, setData, put, processing, errors } = useForm({ 5 name: user.name, 6 email: user.email, 7 }) 8 9 function submit(e) {10 e.preventDefault()11 put(`/users/${user.id}`)12 }13 14 return (15 <form onSubmit={submit}>16 <input17 type="text"18 value={data.name}19 onChange={e => setData('name', e.target.value)}20 />21 {errors.name && <div>{errors.name}</div>}22 23 <button disabled={processing}>Save</button>24 </form>25 )26}
That's it.
No API route. No manual fetch calls. No loading state management. No error state management. The validation happens on the server using Laravel's battle-tested validation, and errors automatically flow to the frontend.
The difference in lines of code is staggering. More importantly, the mental overhead is dramatically lower. You're not context-switching between API design, request handling, error formats, and frontend state management. You're just building your feature.
React developers love TypeScript. Good news: Inertia works beautifully with it.
For shared types between Laravel and React, you can use tools like Laravel TypeScript or Ziggy for route helpers.
Example with type-safe forms:
1import { useForm } from '@inertiajs/react' 2 3interface UserFormData { 4 name: string 5 email: string 6 avatar: File | null 7} 8 9interface User {10 id: number11 name: string12 email: string13 avatar_url: string14}15 16interface Props {17 user: User18}19 20export default function Edit({ user }: Props) {21 const { data, setData, put, errors } = useForm<UserFormData>({22 name: user.name,23 email: user.email,24 avatar: null,25 })26 27 // TypeScript knows all the types28 function submit(e: React.FormEvent) {29 e.preventDefault()30 put(`/users/${user.id}`)31 }32 33 return (34 <form onSubmit={submit}>35 <input36 type="text"37 value={data.name}38 onChange={e => setData('name', e.target.value)}39 />40 </form>41 )42}
Laravel Data and other packages can automatically generate TypeScript definitions from your PHP classes, giving you end-to-end type safety from database to UI.
Here's the beauty of Inertia: you can adopt it incrementally.
Already have a Laravel app with Blade templates? Start converting one page at a time to Inertia + React. Your Blade pages and Inertia pages coexist perfectly.
Already using Vue in your Laravel app? Inertia supports Vue, React, and Svelte you can even mix them in the same project (though you probably shouldn't).
Coming from a pure React background? Laravel is surprisingly approachable. The documentation is excellent, the ecosystem is mature, and the patterns feel intuitive. If you can build a Next.js app, you can build a Laravel app.
When you choose Laravel + Inertia + React, you're not just getting a framework you're getting an ecosystem:
Authentication? Laravel's official React starter kit ships with complete authentication out of the box—login, registration, password reset, email verification, and even two-factor authentication powered by Laravel Fortify. It uses React 19, TypeScript, Tailwind 4, and shadcn/ui components.
Payments? Laravel Cashier handles Stripe subscriptions with a few lines of code.
Background Jobs? Laravel Queues with Horizon for monitoring.
Real-time? Laravel Reverb (WebSockets), Echo (client library), and it integrates seamlessly with Inertia.
Emails? Laravel's Mailable system with beautiful templates.
File Storage? Unified API for local, S3, or any other storage.
Testing? Pest for backend, React Testing Library for frontend, Dusk for E2E.
Deployment? Laravel Forge for one-click deployment to any cloud provider.
Compare this to the JavaScript ecosystem where you're constantly evaluating, integrating, and maintaining dozens of disparate packages. The Laravel ecosystem is cohesive, well-documented, and it just works.
React developers: Laravel wants you.
Taylor Otwell and the Laravel team have been systematically building a modern frontend story with Inertia at its center. This isn't a half-baked attempt to shoehorn React into a PHP framework—it's a thoughtfully designed approach that respects both ecosystems.
Inertia 2.0 represents a fundamental rethinking of how to build modern web applications. Instead of forcing you to become a full-stack JavaScript developer (with all the complexity that entails), it lets you leverage the best of both worlds:
The result? You ship features faster. Your code is simpler. Your application is more maintainable. And you can actually build substantial products as a solo developer or small team.
As Taylor said, Laravel and Rails were built to empower individual developers to build the next GitHub, Airbnb, or Shopify. With Inertia 2.0, React developers can now join that story.
The modern monolith is back. And it's powered by React.
The tools are here. The ecosystem is mature. The only question is: are you ready to simplify your stack?
Written by
Writing and maintaining @LaravelMagazine. Host of "The Laravel Magazine Podcast". Pronouns: vi/vim.
Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.