A dashboard rarely has one kind of data.
Some values are expensive to calculate. Some must stay fresh. Others only matter after the user starts typing.
Inertia 3.x gives you a focused tool for each case:
- Deferred props load data after the initial page render.
-
usePollrefreshes Inertia page data on a schedule. -
useHttpmakes standalone JSON requests without navigation.
The choice affects perceived performance, request volume, and UI behavior. This playbook shows where each tool fits in a Laravel and Vue SPA.
For background, see the Inertia 3.x upgrade guide, the deferred props documentation, and the HTTP requests documentation.
Start with the request’s job
Choose the strategy based on what the data needs to do.
| Requirement | Best fit |
|---|---|
| Load a slow panel after the page shell appears | Deferred props |
| Refresh existing page props at regular intervals | usePoll |
| Query or mutate a JSON endpoint without navigation | useHttp |
This distinction keeps your application easier to reason about.
A deferred prop still belongs to the Inertia page. A poll still reloads page props. A useHttp request is independent from the page lifecycle.

The example: one dashboard, three data shapes
Imagine a SaaS dashboard with three panels:
- A stats panel with revenue, subscriptions, and conversion data.
- A live activity feed with recent orders and payments.
- A search box for finding users and invoices.
Each panel has a different loading requirement.
The stats query may scan large tables or call a reporting service. It should not block the first render.
The activity feed changes throughout the day. Users benefit from regular updates while they keep the dashboard open.
The search box needs a request only after the user enters a query. It should return JSON and avoid changing the current page.
This is where Inertia 3.x lets the architecture follow the UI.
Deferred props: move slow data off the critical path
Deferred props let Laravel render the page shell first. Inertia resolves the deferred value in a follow-up request.
Use them for data that is useful but not required to display the initial interface. Common examples include reports, recommendations, permissions, analytics, and large secondary lists.
On the Laravel side, wrap the expensive work in Inertia::defer():
use Inertia\Inertia;
public function __invoke()
{
return Inertia::render('Dashboard', [
'stats' => Inertia::defer(
fn () => DashboardStats::for(auth()->user()),
rescue: true,
),
'feed' => Activity::query()
->latest()
->limit(20)
->get(),
]);
}
The closure runs after the initial page response. The browser can render navigation, headings, controls, and lightweight data immediately.
The rescue: true option matters for non-critical panels. If the stats query fails, Inertia reports the exception through Laravel’s normal exception handler. It omits the failed prop and marks it as rescued instead of breaking the entire deferred response.
On the Vue side, use <Deferred> to define loading and failure states:
<script setup>
import { Deferred, router } from '@inertiajs/vue3'
defineProps({
stats: Object,
feed: Array,
})
function retryStats() {
router.reload({
only: ['stats'],
})
}
</script>
<template>
<section class="stats-panel">
<Deferred data="stats">
<template #fallback>
<StatsSkeleton />
</template>
<template #rescue="{ reloading }">
<div class="rounded border p-6">
<p>We could not load the dashboard stats.</p>
<button
type="button"
:disabled="reloading"
@click="retryStats"
>
{{ reloading ? 'Retrying…' : 'Try again' }}
</button>
</div>
</template>
<template #default="{ reloading }">
<div :class="{ 'opacity-60': reloading }">
<StatsCards :stats="stats" />
</div>
</template>
</Deferred>
</section>
</template>
The fallback slot handles the first load. The rescue slot handles a failed deferred request.
The reloading value supports a better reload experience. Keep existing content visible during a partial reload instead of replacing it with a full skeleton.
The rescue state remains visible until you explicitly reload the prop. That makes the retry action predictable.

Group deferred work when the timing matters
Inertia fetches deferred props together by default. You can assign group names to fetch certain props in parallel.
return Inertia::render('Dashboard', [
'stats' => Inertia::defer(
fn () => DashboardStats::for(auth()->user()),
rescue: true,
),
'teams' => Inertia::defer(
fn () => Team::for(auth()->user()),
'dashboard-lists',
),
'projects' => Inertia::defer(
fn () => Project::for(auth()->user()),
'dashboard-lists',
),
]);
Use groups when independent panels should resolve separately from one another. Avoid grouping everything by habit. The group boundary should reflect the user experience.
usePoll: refresh page data without building a timer
Polling fits data that belongs to the current Inertia page and changes over time.
The Vue adapter’s usePoll composable performs repeated Inertia reloads. You can limit each request with only, so the live feed does not reload unrelated props.
<script setup>
import { usePoll } from '@inertiajs/vue3'
const poll = usePoll(5000, {
only: ['feed'],
mode: 'cancel',
})
</script>
This polls every five seconds. It reloads only the feed prop.
Polling stops automatically when the component unmounts. You can also call poll.stop() and poll.start() when the user hides the panel, switches tabs, or changes the dashboard scope.
Choose the polling mode deliberately
The interval alone does not define polling behavior. You also need to decide what happens when a request takes longer than the interval.
overlap: start every tick
overlap is the default mode. A new request starts even when the previous request is still running.
usePoll(3000, {
only: ['feed'],
mode: 'overlap',
})
This can work for fast endpoints with small responses. It is less suitable for expensive queries. Slow responses may arrive out of order and add unnecessary server work.
cancel: keep the newest request
cancel aborts the in-flight request when the next tick begins.
usePoll(5000, {
only: ['feed'],
mode: 'cancel',
})
Use it when the latest state matters more than every intermediate state. A live activity feed often fits this model. So do a rapidly changing metrics panel.
rest: wait, then pause
rest prevents overlap. Inertia waits for the current request to finish, then waits for the interval before starting the next request.
usePoll(5000, {
only: ['feed'],
mode: 'rest',
})
Choose this mode for heavy reports or endpoints that should never run concurrently. The actual time between requests grows when the server takes longer to respond.
For most live dashboard feeds, start with cancel. Use rest when protecting server capacity matters more than a strict update cadence. Reserve overlap for endpoints that are quick and safe to query concurrently.
See the Inertia polling documentation for the full option set.

useHttp: fetch JSON without navigation
Some dashboard interactions do not represent page visits.
A search box is a clear example. The user types a phrase, the client requests matching records, and the current page remains unchanged.
Inertia 3.x provides useHttp for this case. It makes standalone HTTP requests and expects JSON responses. It does not trigger an Inertia navigation or participate in the page prop lifecycle.
<script setup>
import { ref } from 'vue'
import { useHttp } from '@inertiajs/vue3'
const results = ref([])
const search = useHttp({
query: '',
})
let timeout
function searchResources() {
clearTimeout(timeout)
timeout = setTimeout(() => {
search.cancel()
search.get('/api/search', {
onSuccess: (data) => {
results.value = data
},
})
}, 250)
}
</script>
<template>
<div>
<input
v-model="search.query"
type="search"
placeholder="Search users and invoices"
@input="searchResources"
/>
<span v-if="search.processing">Searching…</span>
<ul v-else>
<li v-for="result in results" :key="result.id">
{{ result.name }}
</li>
</ul>
</div>
</template>
useHttp exposes reactive state similar to useForm. You get processing, errors, progress, wasSuccessful, and isDirty, along with request methods such as get, post, put, patch, and delete.
The cancel() call helps prevent an older search from winning after a newer query has been entered.
Your Laravel endpoint should return JSON:
Route::get('/search', function (Request $request) {
return response()->json(
User::query()
->where('name', 'like', '%' . $request->string('query') . '%')
->limit(10)
->get(['id', 'name', 'email'])
);
});
For authenticated endpoints, protect the route with your chosen API authentication layer. Laravel’s Sanctum documentation covers common SPA and token-based setups.
This approach also suits small actions, file uploads, and optimistic updates. If you are building a REST API with PHP for a separate client, useHttp can consume those JSON endpoints without forcing your Inertia page to become the transport layer.
A practical decision checklist
Ask these questions before adding a request:
- Does the data need to block the first render? If not, defer it.
-
Does it belong to the current Inertia page? If yes, use a partial reload or
usePoll. -
Does it need regular updates? Use
usePoll. -
Can requests overlap? Choose
overlap,cancel, orrestbased on server cost and data freshness. -
Does the interaction return JSON without navigation? Use
useHttp. - Can the request fail without invalidating the page? Add a rescue path or local error state.
These decisions also help you keep your Laravel controllers focused. Use Laravel’s response tools for clear JSON contracts. Keep expensive work behind explicit closures. Let each Vue component own the loading state it can explain.
Build for the next interaction
Inertia 3.x does not ask you to choose between server-driven pages and client-side behavior. It gives each kind of data a suitable path.
Defer what is slow. Poll what changes. Fetch what is independent.
That simple separation keeps the initial render quick, the live dashboard controlled, and standalone interactions easy to cancel and validate. It also gives PHP developers a clean way to use Laravel’s full-stack strengths while keeping Vue responsive.
The next wave of Laravel applications will not load every value at once. They will deliver the right data at the right moment.