November 15th, 2025

Why React developers are ditching Next.js for Laravel (and you should too)

Why React developers are ditching Next.js for Laravel (and you should too)

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.

The Full-Stack JavaScript Illusion

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.

Enter Inertia: The Modern Monolith

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:

  1. Your Laravel routes return Inertia responses (not JSON)
  2. Inertia sends your React component name + props to the frontend
  3. Your React app renders using those props
  4. On subsequent navigation, Inertia intercepts requests and fetches only the JSON it needs
  5. No full page reloads, no API to build, no state management library required for server data

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.

What's New in Inertia 2.0: A Complete Rewrite

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.

1. Async Requests: Breaking Free from Sequential Limitations

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 requests
2// Disables progress indicator by default
3router.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.

2. Prefetching: Make Navigation Instant

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 hover
4<Link href="/users/123">View Profile</Link>
5 
6// Or prefetch immediately on page load
7<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.

3. Deferred Props: Optimize Initial Load Times

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 immediately
7 'analytics' => Inertia::defer(fn () => $this->getAnalytics()), // Loaded async
8 ]);
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.

4. Lazy Loading with WhenVisible

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.

5. Infinite Scrolling: Built-in Primitives

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.

6. Polling: Real-time Without WebSockets

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.

7. History Encryption: Security by Default

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.

Forms: Where Inertia Shines

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 <input
18 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 <input
25 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 <input
32 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:

  • No API endpoint to build
  • No JSON responses to format
  • No client-side validation duplication
  • No manual error state management
  • No loading state management beyond the provided processing boolean

The 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.

Advanced Form Features

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')

The Developer Experience Difference

Let me show you two approaches to building the same feature: a user profile page with the ability to update information.

The Traditional React + Next.js Approach

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 validation
26 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: body
36 })
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 <input
53 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:

  • Error boundaries
  • Type safety
  • CORS in development
  • Optimistic updates
  • Success notifications

The Laravel + Inertia + React Approach

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 <input
17 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.

Type Safety: The TypeScript Story

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: number
11 name: string
12 email: string
13 avatar_url: string
14}
15 
16interface Props {
17 user: User
18}
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 types
28 function submit(e: React.FormEvent) {
29 e.preventDefault()
30 put(`/users/${user.id}`)
31 }
32 
33 return (
34 <form onSubmit={submit}>
35 <input
36 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.

Migration Path: You Don't Have to Rewrite Everything

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.

The Ecosystem Advantage

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.

The Bottom Line

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:

  • React for what it's great at: Building interactive, component-based UIs
  • Laravel for what it's great at: Routing, validation, authentication, database queries, background jobs, email—all the backend stuff that JavaScript frameworks struggle with

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.


Try It Today

The tools are here. The ecosystem is mature. The only question is: are you ready to simplify your stack?

Statamic Ninja

Comments

Marian Pop

PHP / Laravel Developer. Writing and maintaining @LaravelMagazine. Host of "The Laravel Magazine Podcast". Pronouns: vi/vim.

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.