skip to content

Shared & Deferred Props

Props reach the page from share() in HandleInertiaRequests, from lazy closures, and from optional, deferred and merged helpers loaded later. Interviewers probe what each costs and exposes.

on this pageshow

explore

questions

5

In a Laravel Inertia app, how do you share the signed-in user and an unread-notification count with every page, and at what cost?

level: middleimportance: must knowfreq 55%

answer

  1. one middleware method for every page
  2. spread parent::share() to keep errors
  3. closures run only when a page is built
  4. select fields, not the whole model
  5. page props win on a key clash

basics

~20 s

Return them from share() in HandleInertiaRequests, keeping parent::share(). Every shared prop is sent with every Inertia response and is readable in the browser, so share little, select fields explicitly and wrap costly values in closures.

solid answer

~50 s

`HandleInertiaRequests::share(Request $request)` returns an array that the adapter merges into the props of every Inertia response; spread `parent::share($request)` first so the always-sent `errors` prop survives. For the user, share a selected shape such as `'auth' => ['user' => $request->user()?->only('id', 'name', 'avatar_url')]`, because a whole model is converted with `toArray()` and every visible attribute reaches the page source. Wrap the unread count in a closure, `'notifications' => fn () => ...`, so the query runs only when an Inertia page is actually built and is skipped when a partial reload does not ask for it. The costs: each shared prop rides along on every page and every visit, its query runs on every full response, and a page prop with the same key replaces it. The client reads it through `usePage().props` in any layout or page.

code

php · 25 lines
php
<?php

namespace App\Http\Middleware;

use Illuminate\Http\Request;
use Inertia\Middleware;

class HandleInertiaRequests extends Middleware
{
    protected $rootView = 'app';

    public function share(Request $request): array
    {
        return [
            ...parent::share($request),
            'app' => ['name' => config('app.name')],
            'auth' => [
                'user' => $request->user()?->only('id', 'name', 'avatar_url'),
            ],
            'notifications' => fn () => [
                'unread' => $request->user()?->unreadNotifications()->count() ?? 0,
            ],
        ];
    }
}

go deeper

for a junior

Recall that HandleInertiaRequests::share() makes data available on every page and that the client reads it with usePage().props.

for a middle

Explain why parent::share() matters, how closures defer work, the merge order with page props, and what a whole model exposes.

for a senior

Audit shared props for payload and query cost, select fields explicitly, and know the dot-notation closure detail when tuning partial reloads.

for a principal

Set a team rule for what may be shared globally, balancing convenience in layouts against payload, exposure and coupling.

## Where shared props come from Inertia's Laravel adapter merges **shared props** into every page it renders. The usual home is the app's `HandleInertiaRequests` middleware, published by `php artisan inertia:middleware` and appended to the `web` group in `bootstrap/app.php`. Its `share(Request $request)` method returns an array; the adapter's base middleware calls it on each request and registers the result with `Inertia::share()`. The base `Inertia\Middleware::share()` returns one entry, `errors`, wrapped in `Inertia::always()` so validation messages are present on every response. That is why the published stub starts with `...parent::share($request)`: drop it and the errors prop disappears. You can also call `Inertia::share('key', $value)` anywhere that runs before the response is built, such as a route-specific middleware or a service provider. ## Sharing the user and the notification count For an analytics dashboard with a header avatar and a notification bell, the typical shape is: - `auth.user`: a **selected** set of fields, e.g. `$request->user()?->only('id', 'name', 'avatar_url')`. - `notifications.unread`: a count, computed in a **closure** so the query is not run when nothing needs it. - small static values, such as the app name from `config('app.name')`. On the client, a layout reads them with `usePage().props.auth.user` in React or Vue, so no page has to pass them down. ## What each shared prop costs | Cost | Why | |---|---| | Payload on every response | shared props are merged into every page's props, on the first HTML load and on every JSON visit | | Query time on every full response | a plain value is computed in `share()`, which the middleware calls on every request it handles, Inertia or not | | Exposure | everything in props is readable in the page source, the network tab and the history state | | Key collisions | the adapter merges shared props first and page props second, so a page prop with the same key replaces the shared one | Passing `$request->user()` itself, as the starter kits do, is convenient but sends the model's `toArray()` output: every attribute not hidden on the model, plus appended ones. Selecting fields makes the contract explicit and stops a column added later from reaching every page. ## Closures: when they help and when they do not A closure is not called in the middleware. The adapter calls it while building the response, so: 1. Requests that never produce an Inertia page (a file download, a JSON endpoint in the `web` group) never pay for it. 2. A **partial reload** that asks only for other props skips a top-level closure entirely: `router.reload({ only: ['chart'] })` does not run the notification count. 3. It still runs on every full visit, so an expensive value stays expensive; cache it or move it off the shared list. One detail from the adapter's source: keys written in **dot notation**, such as `'auth.user' => fn () => ...`, are expanded before partial filtering, and closures under them are called during that expansion. Nest arrays instead, for example `'auth' => fn () => ['user' => ...]`, when you want a partial reload to skip the work. ## Keeping the list short - Share what the layout needs on nearly every page: identity, permissions the navigation uses, counts in the header. - Pass page-specific data from the controller instead. - For data that rarely changes, such as a list of countries or teams, consider a once prop, which the client remembers across visits. - Namespace keys (`auth`, `notifications`, `app`) so a page prop called `user` never shadows the signed-in user by accident. ## Diagnosing a slow layout When every page feels slow, shared props are the first suspect, because they are the only props every page pays for: 1. Open any page's JSON visit in the network tab and look at the keys outside the page's own data. 2. Log or profile the queries run while `share()` and its closures execute. 3. Move anything the layout does not render into the pages that need it, wrap the rest in closures, and consider a once prop for stable lists. ## A note on the metadata With the default `expose_shared_prop_keys` setting, each page object also lists the top-level shared keys in `sharedProps`, so the client can carry them over during instant visits. It lists names only, never values.

  • Is a closure under a dot-notation key like 'auth.user' skipped on partial reloads?
    No. The adapter expands dot-notation keys into nested arrays before it applies the partial-reload filter, and calls closures under those keys while expanding them. The value is then dropped if not requested, but the work has been done. A top-level closure, such as `'auth' => fn () => ['user' => ...]`, is only called when the reload includes `auth`.
  • What happens if a controller passes a prop with the same key as a shared prop?
    The page prop wins for that response. The adapter merges shared props first and page props second with a plain `array_merge`, so the controller's value replaces the shared one entirely, not recursively. Namespacing shared keys such as `auth` and `notifications` avoids shadowing them by accident.
  • Why must share() spread parent::share($request)?
    The base middleware's `share()` registers `errors` as an always prop built from the session's validation errors. Returning an array without it means forms stop receiving validation messages, and nothing fails loudly. The published stub starts with the spread for that reason.

saying these in an interview costs you the question

  • Shared props are sent once and cached by the client for later pages.
  • A closure in share() keeps the value out of the page source.
  • Passing $request->user() sends only the fields in $fillable.
  • Shared values override page props with the same key.
  • share() only runs for requests that return an Inertia page.
open as a page

With Inertia on Laravel, how do you show a one-time 'report exported' toast with Inertia::flash, and why not share it as a regular prop?

level: juniorimportance: should knowfreq 35%

basics

~20 s

Call Inertia::flash('toast', [...]) and return back() (or chain ->back()); the next page object carries it under flash, outside props. Because flash is not stored in history state, the toast does not reappear when the user presses Back.

open as a page

With Inertia on Laravel, how do a plain value, a closure, Inertia::optional and Inertia::always behave on a full visit versus a partial reload?

level: middleimportance: should knowfreq 45%

basics

~20 s

A plain value is always computed, then dropped if a partial reload excludes it. A closure runs only when included. Inertia::optional is sent only when a partial reload asks for it. Inertia::always is sent every time.

open as a page

A Laravel Inertia dashboard waits two seconds on a revenue chart query; how would you use Inertia::defer and <Deferred>, and what does it cost?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Wrap the chart in Inertia::defer(fn () => ...): the first response only announces it, the client fetches it in a follow-up partial reload, and <Deferred> shows a fallback. Costs: extra requests that re-run the controller, and no chart in the first HTML.

open as a page

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%

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.

open as a page