Inertia 2.0 completely rewrote how requests work. Everything is async now. Multiple requests can happen at the same time without blocking each other.
This might sound like a small change, but it's not. It's the foundation that makes every other new feature possible—prefetching, deferred props, polling, lazy loading.
If you're still using Inertia 1.0 patterns in your 2.0 apps, you're missing out on some genuinely game-changing stuff. Let me show you what's actually possible now.
In Inertia 1.0, if you made a request, everything else had to wait. Navigate to a new page? Any ongoing request gets cancelled. Reload some data? Better hope nothing else needs to load at the same time.
Inertia 2.0 threw all that out and rebuilt it from scratch. Now:
This unlocks patterns that were either impossible or required hacky workarounds before.
This one's my favourite. When users hover over a link, Inertia fetches the data in the background. When they click, the page appears instantly because everything's already loaded.
1import { Link } from '@inertiajs/react' 2 3export default function Navigation() { 4 return ( 5 <nav> 6 <Link href="/dashboard" prefetch> 7 Dashboard 8 </Link> 9 <Link href="/users" prefetch>10 Users11 </Link>12 <Link href="/settings" prefetch>13 Settings14 </Link>15 </nav>16 )17}
That's it. Add prefetch to your links and they become instant.
By default, Inertia waits 75ms after you hover before fetching. This avoids making requests when someone just moves their mouse across the screen. But if they pause on a link for a moment, the data loads silently in the background.
You can control exactly when prefetching happens:
1// Prefetch immediately when the page loads 2<Link href="/dashboard" prefetch="mount"> 3 Dashboard 4</Link> 5 6// Prefetch on click (mousedown, before the actual navigation) 7<Link href="/users" prefetch="click"> 8 Users 9</Link>10 11// Prefetch on hover (default, but you can be explicit)12<Link href="/settings" prefetch="hover">13 Settings14</Link>15 16// Combine multiple strategies17<Link href="/dashboard" prefetch={['mount', 'hover']}>18 Dashboard19</Link>
When to use each:
mount - For pages users almost always visit (like the dashboard)hover - For navigation links (the default)click - For links where prefetching on hover wastes bandwidthPrefetched data stays cached for 30 seconds by default. After that, it's refetched. You can customise this:
1// Cache for 1 minute 2<Link href="/users" prefetch cacheFor="1m"> 3 Users 4</Link> 5 6// Cache for 10 seconds 7<Link href="/dashboard" prefetch cacheFor="10s"> 8 Dashboard 9</Link>10 11// Cache for 5000 milliseconds (5 seconds)12<Link href="/settings" prefetch cacheFor={5000}>13 Settings14</Link>
Even if the cache expires, the page still loads instantly with the old data, then updates once the fresh data arrives. Users never see a loading state.
Sometimes you want to prefetch without a link:
1import { router } from '@inertiajs/react' 2 3function prefetchNextPages(currentPage: number) { 4 // Prefetch the next 2 pages 5 router.prefetch(`/posts?page=${currentPage + 1}`) 6 router.prefetch(`/posts?page=${currentPage + 2}`, {}, { 7 cacheFor: '1m' 8 }) 9}10 11export default function Posts({ posts, currentPage }: Props) {12 useEffect(() => {13 prefetchNextPages(currentPage)14 }, [currentPage])15 16 return (17 <div>18 {posts.map(post => <PostCard key={post.id} {...post} />)}19 </div>20 )21}
This is brilliant. You can tag cached data and invalidate everything with that tag at once:
1// Tag prefetched data 2<Link href="/users" prefetch cacheTags="users"> 3 Users 4</Link> 5 6<Link href="/dashboard" prefetch cacheTags={['dashboard', 'stats']}> 7 Dashboard 8</Link> 9 10// When you create a user, invalidate the users cache11import { useForm } from '@inertiajs/react'12 13const form = useForm({ name: '', email: '' })14 15form.post('/users', {16 invalidateCacheTags: ['users', 'dashboard']17})
Now when the form succeeds, any prefetched data tagged with users or dashboard gets cleared. The next time someone hovers those links, they get fresh data.
This is where async requests really shine. You can send the page immediately with the important data, then load expensive queries in the background.
Imagine a dashboard that shows:
Without deferred props, users wait 5+ seconds staring at a blank screen.
1// Laravel Controller 2public function dashboard() 3{ 4 return Inertia::render('Dashboard', [ 5 // These load immediately 6 'user' => Auth::user(), 7 'stats' => [ 8 'revenue' => $quickRevenueQuery, 9 'customers' => $quickCustomerCount,10 ],11 12 // These load in the background13 'analytics' => Inertia::defer(fn() => $expensiveAnalyticsQuery),14 'activity' => Inertia::defer(fn() => $slowActivityQuery),15 ]);16}
1// React Component 2interface DashboardProps { 3 user: User 4 stats: { 5 revenue: number 6 customers: number 7 } 8 // Optional because they load later 9 analytics?: AnalyticsData10 activity?: ActivityData11}12 13export default function Dashboard({ user, stats, analytics, activity }: DashboardProps) {14 return (15 <div>16 <h1>Welcome back, {user.name}</h1>17 18 {/* Always available */}19 <div className="grid grid-cols-2 gap-4">20 <StatCard label="Revenue" value={stats.revenue} />21 <StatCard label="Customers" value={stats.customers} />22 </div>23 24 {/* Shows loading state until data arrives */}25 {analytics ? (26 <AnalyticsChart data={analytics} />27 ) : (28 <Skeleton className="h-64" />29 )}30 31 {activity ? (32 <ActivityFeed items={activity} />33 ) : (34 <Skeleton className="h-48" />35 )}36 </div>37 )38}
The page renders immediately with user info and stats. Analytics and activity load in separate requests in the background. Users see content instantly instead of waiting for everything.
You can group deferred props to control parallelism:
1return Inertia::render('Dashboard', [ 2 'user' => Auth::user(), 3 4 // Group 1 - loads together 5 'teams' => Inertia::defer(fn() => $teams)->group('sidebar'), 6 'projects' => Inertia::defer(fn() => $projects)->group('sidebar'), 7 8 // Group 2 - loads in parallel with group 1 9 'tasks' => Inertia::defer(fn() => $tasks)->group('main'),10 11 // No group - loads separately12 'permissions' => Inertia::defer(fn() => $permissions),13]);
This makes 3 requests total: one for sidebar (teams + projects), one for main (tasks), and one for permissions.
For more control, use the Deferred component:
1import { Deferred } from '@inertiajs/react' 2 3export default function Dashboard({ user, analytics }: Props) { 4 return ( 5 <div> 6 <h1>Welcome {user.name}</h1> 7 8 <Deferred data="analytics" fallback={<LoadingSpinner />}> 9 {(analytics) => <AnalyticsChart data={analytics} />}10 </Deferred>11 </div>12 )13}
Wait for multiple props:
1<Deferred data={['analytics', 'activity']} fallback={<LoadingSpinner />}>2 {(analytics, activity) => (3 <div>4 <AnalyticsChart data={analytics} />5 <ActivityFeed items={activity} />6 </div>7 )}8</Deferred>
Sometimes you need data to update automatically. Polling is perfect for dashboards, leaderboards, or status pages where WebSockets would be overkill.
1import { usePoll } from '@inertiajs/react' 2 3export default function Leaderboard({ scores }: { scores: Score[] }) { 4 // Poll every 5 seconds 5 usePoll(5000) 6 7 return ( 8 <div> 9 {scores.map(score => (10 <div key={score.id}>11 {score.player}: {score.points}12 </div>13 ))}14 </div>15 )16}
That's it. Every 5 seconds, Inertia reloads the page data. No page refresh, no flicker, just updated data.
By default, usePoll reloads everything. Usually you only want to update specific data:
1usePoll(5000, {2 only: ['scores'] // Only reload scores, nothing else3})
This is crucial. Without only, you're reloading your authenticated user, flash messages, everything. That's wasteful.
Sometimes you want to control when polling starts:
1import { usePoll } from '@inertiajs/react' 2 3export default function Dashboard({ stats }: Props) { 4 const { start, stop } = usePoll(3000, 5 { only: ['stats'] }, 6 { autoStart: false } // Don't start automatically 7 ) 8 9 return (10 <div>11 <button onClick={start}>Start Live Updates</button>12 <button onClick={stop}>Pause Updates</button>13 14 <div>{stats.activeUsers} users online</div>15 </div>16 )17}
When the browser tab isn't visible, Inertia automatically throttles polling by 90%. This saves server resources and battery life.
If you need polling to continue at full speed even in the background:
1usePoll(5000, { only: ['messages'] }, {2 keepAlive: true // Poll at full speed even when tab is hidden3})
Here's how I use polling in Leadsprout (The SaaS I'm building) for a live stats dashboard:
1import { usePoll } from '@inertiajs/react' 2 3interface DashboardProps { 4 stats: { 5 leadsToday: number 6 leadsThisWeek: number 7 activeScans: number 8 } 9}10 11export default function Dashboard({ stats }: DashboardProps) {12 // Update stats every 10 seconds, only when tab is visible13 usePoll(10000, {14 only: ['stats'],15 preserveScroll: true, // Don't jump to top16 })17 18 return (19 <div className="grid grid-cols-3 gap-4">20 <StatCard21 label="Leads Today"22 value={stats.leadsToday}23 trend="up"24 />25 <StatCard26 label="This Week"27 value={stats.leadsThisWeek}28 />29 <StatCard30 label="Active Scans"31 value={stats.activeScans}32 pulse={stats.activeScans > 0}33 />34 </div>35 )36}
Users see live updates without refreshing. When they switch tabs, polling slows down automatically. When they come back, it speeds up again.
Let's build something real that combines prefetching, deferred props, and polling.
In Leadsprout, we have a page that shows:
1// Controller 2public function leads() 3{ 4 return Inertia::render('Leads/Index', [ 5 // Load immediately 6 'leads' => Lead::with('company') 7 ->latest() 8 ->paginate(50), 9 10 // Defer expensive AI calculations11 'scorings' => Inertia::defer(12 fn() => AIScoring::recent()->get()13 ),14 15 // Poll for active scans16 'activeScans' => ActiveScan::count(),17 ]);18}
1import { usePoll } from '@inertiajs/react' 2import { Deferred } from '@inertiajs/react' 3 4interface LeadsProps { 5 leads: Paginated<Lead> 6 scorings?: Scoring[] 7 activeScans: number 8} 9 10export default function LeadsIndex({ leads, scorings, activeScans }: LeadsProps) {11 // Poll for active scan count every 5 seconds12 usePoll(5000, {13 only: ['activeScans'],14 preserveScroll: true,15 })16 17 return (18 <div>19 <div className="flex justify-between mb-4">20 <h1>Leads</h1>21 {activeScans > 0 && (22 <Badge variant="pulse">23 {activeScans} scans running24 </Badge>25 )}26 </div>27 28 {/* Lead list available immediately */}29 <div className="grid grid-cols-1 gap-4">30 {leads.data.map(lead => (31 <LeadCard key={lead.id} lead={lead} />32 ))}33 </div>34 35 {/* AI scorings load in background */}36 <Deferred37 data="scorings"38 fallback={39 <div className="mt-8">40 <Skeleton className="h-64" />41 <p className="text-sm text-grey-500 mt-2">42 Calculating AI scores...43 </p>44 </div>45 }46 >47 {(scorings) => (48 <div className="mt-8">49 <h2>Recent AI Scores</h2>50 <ScoringResults data={scorings} />51 </div>52 )}53 </Deferred>54 </div>55 )56}
What happens:
All of this would've been a nightmare in Inertia 1.0. Now it's just a few props and hooks.
only with Polling1// Bad - reloads everything2usePoll(5000)3 4// Good - only reloads what changed5usePoll(5000, { only: ['stats'] })
Don't prefetch every link on the page. Focus on:
Use cache tags to automatically invalidate stale data:
1// In your form2form.post('/leads', {3 invalidateCacheTags: ['leads', 'dashboard']4})
Now any prefetched pages tagged with leads or dashboard get fresh data.
If a query takes >500ms, defer it. Users can wait a second if the page is already rendered.
1// Defer anything slow2'analytics' => Inertia::defer(fn() => $expensiveQuery),3 4// Keep fast stuff immediate5'user' => Auth::user(),
Open your browser's network tab. Hover over a prefetched link. You'll see the request happen immediately.
1import { router } from '@inertiajs/react'2 3// Flush all prefetch cache4router.flushAll()5 6// Flush specific page7router.flush('/users')
Add callbacks to see when polling happens:
1usePoll(5000, {2 onStart: () => console.log('Polling started'),3 onFinish: () => console.log('Polling finished'),4 only: ['stats']5})
Inertia 2.0's async architecture isn't just a technical improvement. It fundamentally changes what you can build.
Pages that would've felt slow in 1.0 are now instant. Features that required WebSockets now work with simple polling. Dashboards that needed complex state management just work with deferred props.
The best part? None of this requires learning new concepts. If you know how to use Link and props, you already know 90% of it. Just add prefetch, use Inertia::defer(), or call usePoll().
Start small. Pick one page and add prefetching to the nav. See how much faster it feels. Then try deferred props on a slow dashboard. You'll wonder how you ever built without them.
I'm using all of these patterns in LeadSprout. The lead scoring page went from 3+ seconds to instant with deferred props. The dashboard stays updated with polling. Navigation feels instant with prefetching. This stuff actually works in production.
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.