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
import { Link } from '@inertiajs/react'
export default function Navigation() {
return (
<nav>
<Link href="/dashboard" prefetch>
Dashboard
</Link>
<Link href="/users" prefetch>
Users
</Link>
<Link href="/settings" prefetch>
Settings
</Link>
</nav>
)
}
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:
// Prefetch immediately when the page loads
<Link href="/dashboard" prefetch="mount">
Dashboard
</Link>
// Prefetch on click (mousedown, before the actual navigation)
<Link href="/users" prefetch="click">
Users
</Link>
// Prefetch on hover (default, but you can be explicit)
<Link href="/settings" prefetch="hover">
Settings
</Link>
// Combine multiple strategies
<Link href="/dashboard" prefetch={['mount', 'hover']}>
Dashboard
</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:
// Cache for 1 minute
<Link href="/users" prefetch cacheFor="1m">
Users
</Link>
// Cache for 10 seconds
<Link href="/dashboard" prefetch cacheFor="10s">
Dashboard
</Link>
// Cache for 5000 milliseconds (5 seconds)
<Link href="/settings" prefetch cacheFor={5000}>
Settings
</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:
import { router } from '@inertiajs/react'
function prefetchNextPages(currentPage: number) {
// Prefetch the next 2 pages
router.prefetch(`/posts?page=${currentPage + 1}`)
router.prefetch(`/posts?page=${currentPage + 2}`, {}, {
cacheFor: '1m'
})
}
export default function Posts({ posts, currentPage }: Props) {
useEffect(() => {
prefetchNextPages(currentPage)
}, [currentPage])
return (
<div>
{posts.map(post => <PostCard key={post.id} {...post} />)}
</div>
)
}
Cache Tags for Invalidation
This is brilliant. You can tag cached data and invalidate everything with that tag at once:
// Tag prefetched data
<Link href="/users" prefetch cacheTags="users">
Users
</Link>
<Link href="/dashboard" prefetch cacheTags={['dashboard', 'stats']}>
Dashboard
</Link>
// When you create a user, invalidate the users cache
import { useForm } from '@inertiajs/react'
const form = useForm({ name: '', email: '' })
form.post('/users', {
invalidateCacheTags: ['users', 'dashboard']
})
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
// Laravel Controller
public function dashboard()
{
return Inertia::render('Dashboard', [
// These load immediately
'user' => Auth::user(),
'stats' => [
'revenue' => $quickRevenueQuery,
'customers' => $quickCustomerCount,
],
// These load in the background
'analytics' => Inertia::defer(fn() => $expensiveAnalyticsQuery),
'activity' => Inertia::defer(fn() => $slowActivityQuery),
]);
}
// React Component
interface DashboardProps {
user: User
stats: {
revenue: number
customers: number
}
// Optional because they load later
analytics?: AnalyticsData
activity?: ActivityData
}
export default function Dashboard({ user, stats, analytics, activity }: DashboardProps) {
return (
<div>
<h1>Welcome back, {user.name}</h1>
{/* Always available */}
<div className="grid grid-cols-2 gap-4">
<StatCard label="Revenue" value={stats.revenue} />
<StatCard label="Customers" value={stats.customers} />
</div>
{/* Shows loading state until data arrives */}
{analytics ? (
<AnalyticsChart data={analytics} />
) : (
<Skeleton className="h-64" />
)}
{activity ? (
<ActivityFeed items={activity} />
) : (
<Skeleton className="h-48" />
)}
</div>
)
}
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:
return Inertia::render('Dashboard', [
'user' => Auth::user(),
// Group 1 - loads together
'teams' => Inertia::defer(fn() => $teams)->group('sidebar'),
'projects' => Inertia::defer(fn() => $projects)->group('sidebar'),
// Group 2 - loads in parallel with group 1
'tasks' => Inertia::defer(fn() => $tasks)->group('main'),
// No group - loads separately
'permissions' => Inertia::defer(fn() => $permissions),
]);
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:
import { Deferred } from '@inertiajs/react'
export default function Dashboard({ user, analytics }: Props) {
return (
<div>
<h1>Welcome {user.name}</h1>
<Deferred data="analytics" fallback={<LoadingSpinner />}>
{(analytics) => <AnalyticsChart data={analytics} />}
</Deferred>
</div>
)
}
Wait for multiple props:
<Deferred data={['analytics', 'activity']} fallback={<LoadingSpinner />}>
{(analytics, activity) => (
<div>
<AnalyticsChart data={analytics} />
<ActivityFeed items={activity} />
</div>
)}
</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
import { usePoll } from '@inertiajs/react'
export default function Leaderboard({ scores }: { scores: Score[] }) {
// Poll every 5 seconds
usePoll(5000)
return (
<div>
{scores.map(score => (
<div key={score.id}>
{score.player}: {score.points}
</div>
))}
</div>
)
}
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:
usePoll(5000, {
only: ['scores'] // Only reload scores, nothing else
})
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:
import { usePoll } from '@inertiajs/react'
export default function Dashboard({ stats }: Props) {
const { start, stop } = usePoll(3000,
{ only: ['stats'] },
{ autoStart: false } // Don't start automatically
)
return (
<div>
<button onClick={start}>Start Live Updates</button>
<button onClick={stop}>Pause Updates</button>
<div>{stats.activeUsers} users online</div>
</div>
)
}
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:
usePoll(5000, { only: ['messages'] }, {
keepAlive: true // Poll at full speed even when tab is hidden
})
Real Example: Live Dashboard
Here's how I use polling in Leadsprout (The SaaS I'm building) for a live stats dashboard:
import { usePoll } from '@inertiajs/react'
interface DashboardProps {
stats: {
leadsToday: number
leadsThisWeek: number
activeScans: number
}
}
export default function Dashboard({ stats }: DashboardProps) {
// Update stats every 10 seconds, only when tab is visible
usePoll(10000, {
only: ['stats'],
preserveScroll: true, // Don't jump to top
})
return (
<div className="grid grid-cols-3 gap-4">
<StatCard
label="Leads Today"
value={stats.leadsToday}
trend="up"
/>
<StatCard
label="This Week"
value={stats.leadsThisWeek}
/>
<StatCard
label="Active Scans"
value={stats.activeScans}
pulse={stats.activeScans > 0}
/>
</div>
)
}
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)
// Controller
public function leads()
{
return Inertia::render('Leads/Index', [
// Load immediately
'leads' => Lead::with('company')
->latest()
->paginate(50),
// Defer expensive AI calculations
'scorings' => Inertia::defer(
fn() => AIScoring::recent()->get()
),
// Poll for active scans
'activeScans' => ActiveScan::count(),
]);
}
import { usePoll } from '@inertiajs/react'
import { Deferred } from '@inertiajs/react'
interface LeadsProps {
leads: Paginated<Lead>
scorings?: Scoring[]
activeScans: number
}
export default function LeadsIndex({ leads, scorings, activeScans }: LeadsProps) {
// Poll for active scan count every 5 seconds
usePoll(5000, {
only: ['activeScans'],
preserveScroll: true,
})
return (
<div>
<div className="flex justify-between mb-4">
<h1>Leads</h1>
{activeScans > 0 && (
<Badge variant="pulse">
{activeScans} scans running
</Badge>
)}
</div>
{/* Lead list available immediately */}
<div className="grid grid-cols-1 gap-4">
{leads.data.map(lead => (
<LeadCard key={lead.id} lead={lead} />
))}
</div>
{/* AI scorings load in background */}
<Deferred
data="scorings"
fallback={
<div className="mt-8">
<Skeleton className="h-64" />
<p className="text-sm text-grey-500 mt-2">
Calculating AI scores...
</p>
</div>
}
>
{(scorings) => (
<div className="mt-8">
<h2>Recent AI Scores</h2>
<ScoringResults data={scorings} />
</div>
)}
</Deferred>
</div>
)
}
What happens:
- Page loads instantly with lead list
- "X scans running" badge updates every 5 seconds via polling
- AI scorings load in the background (2-3 seconds)
- 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
// Bad - reloads everything
usePoll(5000)
// Good - only reloads what changed
usePoll(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:
// In your form
form.post('/leads', {
invalidateCacheTags: ['leads', 'dashboard']
})
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.
// Defer anything slow
'analytics' => Inertia::defer(fn() => $expensiveQuery),
// Keep fast stuff immediate
'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
import { router } from '@inertiajs/react'
// Flush all prefetch cache
router.flushAll()
// Flush specific page
router.flush('/users')
Monitor Polling
Add callbacks to see when polling happens:
usePoll(5000, {
onStart: () => console.log('Polling started'),
onFinish: () => console.log('Polling finished'),
only: ['stats']
})
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.