November 20th, 2025

Inertia 2.0 Async Patterns

Inertia 2.0 Async Patterns

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.

What Changed (And Why It Matters)

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:

  • Multiple requests run in parallel
  • Nothing blocks anything else
  • You can fetch data whenever you need it
  • The page stays responsive no matter what's loading

This unlocks patterns that were either impossible or required hacky workarounds before.

Prefetching: Make Navigation Feel Instant

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.

Basic Usage

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 Users
11 </Link>
12 <Link href="/settings" prefetch>
13 Settings
14 </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.

Customising When to Prefetch

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 Settings
14</Link>
15 
16// Combine multiple strategies
17<Link href="/dashboard" prefetch={['mount', 'hover']}>
18 Dashboard
19</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 bandwidth

Cache Duration

Prefetched 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 Settings
14</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.

Programmatic Prefetching

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}

Cache Tags for Invalidation

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 cache
11import { 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.

Deferred Props: Load Heavy Data in the Background

This is where async requests really shine. You can send the page immediately with the important data, then load expensive queries in the background.

The Problem

Imagine a dashboard that shows:

  • User info (fast query)
  • Quick stats (fast)
  • Analytics charts (3 second query)
  • Recent activity (2 second query)

Without deferred props, users wait 5+ seconds staring at a blank screen.

The Solution

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 background
13 '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?: AnalyticsData
10 activity?: ActivityData
11}
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.

Grouping Deferred Props

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 separately
12 'permissions' => Inertia::defer(fn() => $permissions),
13]);

This makes 3 requests total: one for sidebar (teams + projects), one for main (tasks), and one for permissions.

The Deferred Component

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>

Polling: Real-Time Updates Without WebSockets

Sometimes you need data to update automatically. Polling is perfect for dashboards, leaderboards, or status pages where WebSockets would be overkill.

Basic Usage

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.

Only Poll Specific Props

By default, usePoll reloads everything. Usually you only want to update specific data:

1usePoll(5000, {
2 only: ['scores'] // Only reload scores, nothing else
3})

This is crucial. Without only, you're reloading your authenticated user, flash messages, everything. That's wasteful.

Manual Control

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}

Background Throttling

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 hidden
3})

Real Example: Live Dashboard

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 visible
13 usePoll(10000, {
14 only: ['stats'],
15 preserveScroll: true, // Don't jump to top
16 })
17 
18 return (
19 <div className="grid grid-cols-3 gap-4">
20 <StatCard
21 label="Leads Today"
22 value={stats.leadsToday}
23 trend="up"
24 />
25 <StatCard
26 label="This Week"
27 value={stats.leadsThisWeek}
28 />
29 <StatCard
30 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.

Combining Everything: A Real Feature

Let's build something real that combines prefetching, deferred props, and polling.

Use Case: Lead Scoring Dashboard

In Leadsprout, we have a page that shows:

  • Lead list (loads immediately)
  • AI scoring results (deferred, takes 2-3 seconds)
  • Real-time status of ongoing scans (polling)
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 calculations
11 'scorings' => Inertia::defer(
12 fn() => AIScoring::recent()->get()
13 ),
14 
15 // Poll for active scans
16 '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 seconds
12 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 running
24 </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 <Deferred
37 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:

  1. Page loads instantly with lead list
  2. "X scans running" badge updates every 5 seconds via polling
  3. AI scorings load in the background (2-3 seconds)
  4. When user hovers "next page", it prefetches (instant navigation)

All of this would've been a nightmare in Inertia 1.0. Now it's just a few props and hooks.

Performance Tips

1. Always Use only with Polling

1// Bad - reloads everything
2usePoll(5000)
3 
4// Good - only reloads what changed
5usePoll(5000, { only: ['stats'] })

2. Prefetch Strategically

Don't prefetch every link on the page. Focus on:

  • Main navigation
  • Common user paths
  • Slow pages that benefit most

3. Cache Tags Keep Data Fresh

Use cache tags to automatically invalidate stale data:

1// In your form
2form.post('/leads', {
3 invalidateCacheTags: ['leads', 'dashboard']
4})

Now any prefetched pages tagged with leads or dashboard get fresh data.

4. Defer Expensive Queries

If a query takes >500ms, defer it. Users can wait a second if the page is already rendered.

1// Defer anything slow
2'analytics' => Inertia::defer(fn() => $expensiveQuery),
3 
4// Keep fast stuff immediate
5'user' => Auth::user(),

When NOT to Use These Features

Don't Prefetch If:

  • The page requires POST data
  • The data is user-specific and changes constantly
  • You have strict privacy requirements

Don't Poll If:

  • Updates happen rarely (use manual refresh)
  • You need instant updates (use WebSockets/Reverb)
  • The query is expensive (you'll hammer your database)

Don't Defer If:

  • The data is critical for the page
  • The query is already fast (<100ms)
  • Users need it immediately

Debugging

See What's Being Prefetched

Open your browser's network tab. Hover over a prefetched link. You'll see the request happen immediately.

Check Cache Status

1import { router } from '@inertiajs/react'
2 
3// Flush all prefetch cache
4router.flushAll()
5 
6// Flush specific page
7router.flush('/users')

Monitor Polling

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

The Bottom Line

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.

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.