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?
answer
- partial reload: same component, only/except
- a plain value is computed anyway
- closure: evaluated only when included
- optional: never on a full visit
- always: ignores only and except
basics
~20 sA 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.
solid answer
~40 sA **partial reload** is a visit to the same page component with `only` or `except`, for example `router.reload({ only: ['orders'] })`; the client sends `X-Inertia-Partial-Component` plus `X-Inertia-Partial-Data` or `X-Inertia-Partial-Except`. A plain value such as `Order::latest()->get()` runs in the controller before `render()` is called, so it costs a query even when the reload drops it. A closure, `fn () => Order::latest()->get()`, is included on full visits but called only when the response includes it. `Inertia::optional(fn () => ...)` is never included on a full visit and resolves only when a partial reload selects it; in Inertia 3 it replaces the removed `Inertia::lazy()`. `Inertia::always(...)` is included even when a reload's `only` list leaves it out, which is how the adapter shares `errors`.
code
php · 20 lines<?php
namespace App\Http\Controllers;
use App\Models\Order;
use Inertia\Inertia;
use Inertia\Response;
class OrderController extends Controller
{
public function index(): Response
{
return Inertia::render('Orders/Index', [
'statuses' => ['open', 'shipped', 'refunded'],
'orders' => fn () => Order::latest()->paginate(25),
'exportPreview' => Inertia::optional(fn () => Order::exportPreview()),
'canExport' => Inertia::always(fn () => request()->user()->can('export', Order::class)),
]);
}
}go deeper
Recall that router.reload({ only: [...] }) fetches some props, and that wrapping queries in closures avoids running them when not requested.
Explain the four prop shapes across full visits and partial reloads, and that Inertia::optional replaced Inertia::lazy in version 3.
Spot eager queries in controllers that partial reloads, polling and deferred requests re-run, and keep always props small.
Set conventions so pages default to closures and reserve always props, keeping per-visit server cost predictable as pages grow.
## Partial reloads in one paragraph A **partial reload** asks the server for a subset of the current page's props. The client triggers it with `router.reload({ only: ['orders'] })`, a `<Link only={['orders']}>`, or any visit with `only` or `except`. It sends `X-Inertia-Partial-Component` with the current component name, plus `X-Inertia-Partial-Data` (the `only` list) or `X-Inertia-Partial-Except`. The adapter treats the request as partial only when that component name equals the one the controller renders; if the visit ends somewhere else, for example on a login page after a redirect, a full set of props is sent. ## The four prop shapes | Prop | Full visit | Partial reload | When the work runs | |---|---|---|---| | `'orders' => Order::latest()->get()` | sent | sent only if selected | always, before `render()` | | `'orders' => fn () => Order::latest()->get()` | sent | sent only if selected | only when included | | `'orders' => Inertia::optional(fn () => ...)` | **not** sent, not announced | sent only if selected | only when selected | | `'orders' => Inertia::always(...)` | sent | **always** sent | every response | The first row is the classic performance trap. PHP evaluates the array literal before `Inertia::render()` is even called, so the query runs on every request to the action, including a reload that asked only for `chart`. The adapter can drop the result but cannot un-run the query. ## Closures Wrapping values in closures is the cheap default for anything that costs a query. The adapter calls the closure while it builds the response, and only for props that survive the partial filter. On a full visit every closure still runs, so closures do not make a page faster on first load; they make partial reloads cheap. ## `Inertia::optional` Use it for data the page only needs after a user action, such as the rows behind an "export preview" button. On a full visit the prop is neither sent nor announced, and its closure never runs. A partial reload that selects it resolves it: `router.reload({ only: ['exportPreview'] })`. In the source, a reload that sends only an `except` list also includes it unless it is excluded, because the prop then counts as selected. **Inertia 3 note:** `Inertia::lazy()` and the `LazyProp` class, deprecated in 2.0, are gone in 3.0; `Inertia::optional()` is the drop-in replacement. ## `Inertia::always` An always prop bypasses the partial filter, so it is sent even when a reload lists other props in `only` or names it in `except`. The adapter's base middleware shares `errors` this way, so validation messages stay in sync after every request. Keep always props small: they ride on every polling or deferred-prop request too. ## Where the cost shows up The difference between the shapes compounds on pages that reload often: - A **polling** dashboard that reloads `only: ['orders']` every few seconds pays for every eager expression in the action on every tick. - **Deferred props** are fetched with partial reloads too, so eager siblings are recomputed for each deferred group. - A **filter form** that reloads one list after each change repeats the same waste per change if it is not debounced. Converting the eager expressions to closures is usually a one-line change per prop and removes all three costs. ## Picking one - Default to a **closure** for anything that hits the database or an external service. - Use **`optional`** for data that should not load until asked. - Use **`always`** only for small values that must never be stale, like flags that gate the UI. - Leave cheap scalars as **plain values**. When both `only` and `except` are sent, the `only` list narrows the props first and the `except` list is then removed from that set, so a prop named in both is excluded. ## Checking the behaviour In a feature test, the `assertInertia` helpers can reload the page with `reloadOnly('orders', fn ($page) => ...)` or `reloadExcept(...)`, which sends the partial headers and lets you assert that an optional prop appears only when asked for. In the browser, the network tab shows the partial headers on the request and the reduced `props` object in the JSON reply.
- How does Inertia::merge relate to partial reloads?`Inertia::merge($items)` labels a prop so the client appends the new items to the array it already holds instead of replacing it, which suits a load-more list. Merging only happens on partial reloads; a full visit always replaces the prop, even when it carries the merge label. `->prepend()`, `Inertia::deepMerge()` and a `matchOn` key refine how items are combined.
- Why might a partial reload return every prop instead of the requested one?The adapter treats a request as partial only when `X-Inertia-Partial-Component` equals the component the controller renders. If the action renders a different component, for example after a redirect to a login page, or the reload targets another URL, the request is handled as a full visit and all props are sent.
saying these in an interview costs you the question
- Inertia skips the query for any prop a partial reload did not request.
- Inertia::lazy() is still the way to mark a prop optional in Inertia 3.
- Closures make the first page load faster.
- Inertia::always props can be excluded with except.
- A partial reload can target a different page component.