skip to content

With Inertia on Laravel, when do Inertia::once and shareOnce save work, and how can a remembered once prop go stale?

level: seniorimportance: nice to knowfreq 15%

answer

  1. resolved once, remembered by the client
  2. X-Inertia-Except-Once-Props header
  3. fresh(), until(), as()
  4. forgotten on pages without the prop
  5. null for guests, once when signed in

basics

~20 s

Inertia::once(fn () => ...) resolves a prop once; the client remembers it and lists it in X-Inertia-Except-Once-Props so later visits skip it. It goes stale when the data changes and nothing forces fresh(), an expiry or a partial reload.

solid answer

~50 s

`Inertia::once(fn () => Team::all())` marks a prop to be resolved once. The page object records it in `onceProps` with an optional `expiresAt`, and on later visits the client sends the loaded, unexpired keys in `X-Inertia-Except-Once-Props`; the server then skips the callback and leaves the prop out while the client reuses its copy. `Inertia::shareOnce('teams', fn () => ...)`, or a `shareOnce()` method in `HandleInertiaRequests`, does the same for shared props. It saves a query and payload on every visit for data that rarely changes. Staleness is the trade-off: the server cannot know the client's copy is outdated. Refresh with `->fresh()` or `->fresh($condition)`, bound the lifetime with `->until(now()->addHour())`, or reload it with `router.reload({ only: ['teams'] })`. The classic trap is the signed-in user: share `null` for guests and a once prop only when signed in, so login and logout do not leave an old user remembered.

code

php · 26 lines
php
<?php

namespace App\Http\Middleware;

use App\Models\Team;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Middleware;

class HandleInertiaRequests extends Middleware
{
    public function share(Request $request): array
    {
        return [
            ...parent::share($request),
            'auth' => $request->user()
                ? Inertia::once(fn () => $request->user()->only('id', 'name'))
                : null,
        ];
    }

    public function shareOnce(Request $request): array
    {
        return ['teams' => fn () => Team::get(['id', 'name'])];
    }
}

go deeper

for a junior

Recall that once props are sent once and then reused by the client while it navigates between pages that include them.

for a middle

Explain the onceProps metadata, the X-Inertia-Except-Once-Props header, and the fresh, until and as modifiers.

for a senior

Identify staleness risks such as identity and permissions, apply the null-for-guests pattern, and refresh after mutating actions.

for a principal

Decide which shared data may be remembered on the client, weighing payload savings against staleness and security.

## What a once prop is Some data is needed on many pages, rarely changes and costs something to build: the teams a user can filter by, a country list, a plan catalogue. Sending it on every visit wastes a query and payload. A **once prop** is resolved on the first page that includes it and then **remembered by the client**. ## How the exchange works 1. The server resolves `Inertia::once(fn () => Team::all())` normally and adds an entry to the page object's `onceProps`, mapping a key to `{ prop, expiresAt }`. 2. On the next Inertia visit, the client sends `X-Inertia-Except-Once-Props: teams`, listing once props it holds and that have not expired. 3. The adapter sees the key, **skips the callback** and omits the prop from `props`, while still emitting its `onceProps` entry. 4. The client keeps using its stored value. Two boundaries apply: - A first HTML load, with no `X-Inertia` header, always resolves once props. - The client remembers a once prop only while it moves between pages that include it; visiting a page without it forgets the value, and the next page that has it resolves it again. ## Declaring them | Where | How | |---|---| | A page prop | `'teams' => Inertia::once(fn () => Team::all())` | | Shared, inline | `Inertia::share('teams', Inertia::once(fn () => ...))` | | Shared, shortcut | `Inertia::shareOnce('teams', fn () => ...)` | | Middleware | a `shareOnce(Request $request): array` method in `HandleInertiaRequests` | | Combined | `Inertia::defer(fn () => ...)->once()`, and likewise on merge and optional props | ## Controlling freshness - `->fresh()` or `->fresh($condition)` forces the server to resolve and send the prop even when the client listed it as loaded; the new value and expiry replace the client's copy. - `->until($time)` sets an expiry from a `DateTimeInterface`, a `DateInterval` or a number of seconds; after it passes, the client stops listing the key. - `->as('teams')` gives a custom key, so two pages with different prop names, such as `teamFilter` and `availableTeams`, share one remembered value. - A **partial reload** that requests the prop always resolves it, because the except-once header is ignored on partial reloads. ## How once props go stale The server has no view of the client's copy, so staleness is the price of the saving: 1. **Data changes elsewhere.** A colleague creates a team; this user's filter keeps the old list until an expiry, a `fresh()` condition or a reload. 2. **Identity changes.** Sharing `'auth' => Inertia::once(fn () => $request->user())` looks attractive, but login and logout usually redirect to a page that does not explicitly request `auth`, so the remembered user survives. The documented pattern returns `null` for guests, which overwrites the remembered value, and a once prop only when signed in. 3. **Actions on the same page.** After renaming a team, flash a success toast and return a response that marks the prop `fresh()`, or have the client reload it. ## Observing it in the browser The network tab makes the exchange visible. The first Inertia visit to a page with `teams` returns the value and an `onceProps.teams` entry. The next visit's request carries `X-Inertia-Except-Once-Props: teams`, and its response has the `onceProps` entry but no `teams` in `props`. If the header is missing, check that the previous page included the prop and that `expiresAt` has not passed. ## When not to use it - Values that change on most requests, such as an unread-notification count. - Anything security-sensitive whose staleness matters, such as permissions after a role change, unless it has a short `until()` and is refreshed on the actions that change it. - Tiny values where the header and bookkeeping cost more than the saving.

  • Why does a once prop get resolved again after visiting a page that does not include it?
    The client remembers once props only while navigating between pages that include them. When a visited page lacks the prop, the stored value is forgotten, so the next page that includes it does not list it in `X-Inertia-Except-Once-Props`, and the server resolves it again. Sharing the prop, or giving related pages the same key with `as()`, keeps it alive.
  • How do once props interact with a partial reload that requests them?
    A partial reload that names a once prop always resolves it; the adapter ignores the except-once list on partial reloads. That makes `router.reload({ only: ['teams'] })` the client-side way to refresh a remembered value, for example after the user creates a team in a modal.

A regular at a cafe tells the waiter "I still have a menu", so no new one is brought. If the kitchen changes the menu, the regular keeps reading the old one until the waiter is told to hand over a fresh copy. The client's header is the regular's remark; fresh() is the instruction to bring a new menu anyway.

saying these in an interview costs you the question

  • Once props are cached on the server between requests.
  • A once prop is resolved only once per session, even on full reloads.
  • Calling Inertia::flushShared() on logout clears the client's copy.
  • until() accepts only a number of minutes.
  • Once props suit values that change on most requests.