Laravel Daily's

The Inertia 3.x Data Playbook: Defer, Poll, or Fetch in Your Laravel + Vue SPA

hero image

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.
  • usePoll refreshes Inertia page data on a schedule.
  • useHttp makes 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.

Illustration of one dashboard using three loading strategies for stats, a live activity feed, and JSON API search

The example: one dashboard, three data shapes

Imagine a SaaS dashboard with three panels:

  1. A stats panel with revenue, subscriptions, and conversion data.
  2. A live activity feed with recent orders and payments.
  3. 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.

Illustration of Inertia 3.x deferred props with a fast dashboard shell, loading state, and rescue retry action

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.

Illustration of Inertia 3.x polling modes showing overlap, cancel, and rest request behavior

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, or rest based 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.

Previous
Real-Time Laravel + Vue + Inertia: Adding Live Updates with Reverb and Echo
Next
Agent Run Observability in Laravel AI SDK 0.11: Trace Every Step, Tool Call, and Failure