skip to content

With Inertia's Laravel adapter, what does Inertia::render return, and how does the response differ between a first page load and a later client-side visit?

level: juniorimportance: must knowfreq 62%

answer

  1. one controller, two representations
  2. component, props, url, version
  3. root view app.blade.php on a full load
  4. script type=application/json beside the mount div
  5. X-Inertia: true gets bare JSON

basics

~20 s

Inertia::render returns an Inertia\Response that becomes a page object: component, props, url and version. A normal browser request gets the root Blade view with that object embedded as JSON; a request sending X-Inertia: true gets the bare object as JSON.

solid answer

~40 s

`Inertia::render('Properties/Index', [...])`, or the `inertia()` helper, returns an `Inertia\Response`. It implements `Responsable`, so props are resolved only when Laravel converts it into an HTTP response. At that point the adapter builds the **page object**: `component`, `props` (shared props such as `errors` included), `url` and the asset `version`, plus optional keys like `deferredProps` or `encryptHistory` when they apply. A request without the `X-Inertia` header (first load, refresh, pasted link) gets the root view, `app.blade.php` by default, where `<x-inertia::app />` or `@inertia` prints the page object inside a `<script type="application/json">` next to the mount `<div>`. Every visit the client makes afterwards sends `X-Inertia: true`, and the same controller answers with a `JsonResponse` holding only the page object plus an `X-Inertia: true` response header. The client then swaps the page component without reloading the document.

code

php · 20 lines
php
<?php

namespace App\Http\Controllers;

use App\Models\Property;
use Inertia\Inertia;
use Inertia\Response;

class PropertyController extends Controller
{
    public function index(): Response
    {
        return Inertia::render('Properties/Index', [
            'properties' => Property::query()
                ->select(['id', 'name', 'city', 'vacant_units'])
                ->orderBy('name')
                ->get(),
        ]);
    }
}

go deeper

for a junior

Recall the four page object keys and that one Inertia::render call yields HTML on a full load and JSON on a client-side visit.

for a middle

Explain the X-Inertia request and response headers, the root view receiving $page, and why Inertia\Response being Responsable delays prop resolution.

for a senior

Show you can debug the wire: inspect the embedded script element and the XHR responses, and spot endpoints returning non-Inertia responses to Inertia visits.

for a principal

Frame the protocol as the contract between teams: server-owned routing with client-owned views, and what that means for API reuse and caching.

## What `Inertia::render` actually returns Inertia lets a Laravel app keep **server-side routing and controllers** while the views are React, Vue or Svelte **page components**. The controller does not return Blade and does not return an API payload; it returns `Inertia::render($component, $props)`, or the equivalent `inertia($component, $props)` helper. The return value is an `Inertia\Response`. That class implements Laravel's `Responsable` contract, so nothing is serialized when `render()` runs. Laravel calls `toResponse($request)` later, when it turns the controller's return value into an HTTP response, and only then does the adapter resolve props (including closures) and decide what to send. ## The page object Everything Inertia sends is a **page object**. Four keys are present on every one of them: | Key | Holds | |---|---| | `component` | the page component name, e.g. `Properties/Index` | | `props` | the page data, with shared props such as `errors` merged in | | `url` | the request's path and query string | | `version` | the current asset version string | Other keys appear only when a feature needs them: `deferredProps`, `mergeProps`, `onceProps`, `flash`, `encryptHistory`, `clearHistory` and similar. The client treats a missing optional key as empty or `false`. ## First load: the root view with embedded JSON The first request is an ordinary browser navigation, so it carries **no `X-Inertia` header**. For that request `toResponse()` renders the **root view**, `app.blade.php` unless changed, and passes the page object to it as the `$page` variable. Inside the root view: - `<x-inertia::app />` (or the older `@inertia` directive) prints `<script data-page="app" type="application/json">…</script>` followed by `<div id="app"></div>`. - The JSON is encoded with `JSON_HEX_TAG`, so a `<` or `>` inside a prop cannot close the script element early. - `<x-inertia::head>` (or `@inertiaHead`) prints head tags when SSR produced them. The JavaScript bundle boots, reads that script element, and mounts the named page component with its props. The same controller code produced the HTML; no second request for data is needed. ## Later visits: bare JSON After boot, clicking a `<Link>`, submitting a form or calling `router.visit()` does not reload the document. The client sends an XHR with: 1. `X-Inertia: true` and `X-Requested-With: XMLHttpRequest`. 2. `X-Inertia-Version` carrying the version the page loaded with. 3. `Accept: text/html, application/xhtml+xml`. `toResponse()` sees the `X-Inertia` header and returns `new JsonResponse($page, 200, ['X-Inertia' => 'true'])`. The client checks for that **response** header, applies the page object, swaps the component and pushes a history entry. A response without `X-Inertia` (a `dd()` dump, a plain `response()->json()`) is not treated as a page: the client fires its `httpException` event and, unless a listener cancels it, shows the response in an error dialog. ## Where the middleware fits `HandleInertiaRequests`, the app's subclass of `Inertia\Middleware`, runs before the controller and prepares the factory: it registers shared props (the default `share()` adds `errors`), the asset version, the root view name, and it adds `Vary: X-Inertia` to the response. In PHP code you can ask `$request->inertia()`, a request macro the adapter registers, whether the current request is an Inertia visit, though a controller returning `Inertia::render` rarely needs to branch. ## Common misreadings - **"Inertia is a JSON API."** Pages need no separate data endpoints: the controller that serves the HTML also serves the JSON for every later visit to that URL. - **"The client decides by `Accept`."** The client sends `Accept: text/html, application/xhtml+xml` on every visit; the adapter looks only at `X-Inertia`. - **"Props are serialized when `render()` is called."** They are resolved inside `toResponse()`, which is why closures passed as props run only when the response is actually built. - **"The first load is special code."** It is the same page object; only its envelope differs, which is also why server-side rendering can take that same object and pre-render the markup. A quick check in the browser's developer tools shows both envelopes: view the page source for the embedded script element, then click a link and inspect the XHR response to see the bare object with the `X-Inertia: true` response header. ## Inertia 3 notes - The initial page object always lives in the `<script type="application/json">` element. Inertia 2 put the JSON in the root `<div>`'s `data-page` attribute by default; that approach is gone in 3. - The `<x-inertia::app>` and `<x-inertia::head>` Blade components are new in 3; the directives still work. - After upgrading, clear compiled views (`php artisan view:clear`) because the `@inertia` output changed.

  • How can Laravel code tell whether the current request is an Inertia visit?
    The adapter registers a `$request->inertia()` macro that returns true when the `X-Inertia` header is present. Because the client also sends `X-Requested-With: XMLHttpRequest`, `$request->ajax()` is true as well, but that is not specific to Inertia. A controller that returns `Inertia::render` rarely needs either, since the response object already picks HTML or JSON itself.
  • What changed in Inertia 3 about how the initial page object is embedded?
    Inertia 3 always prints it as the body of a `<script type="application/json" data-page="app">` element placed before the mount `<div>`. Inertia 2 defaulted to a `data-page` attribute on the root `<div>` holding the JSON, and v3 removed that path together with the `future` options. Clear compiled views after upgrading, because the `@inertia` directive's output changed.
  • What happens when a controller answers an Inertia visit with response()->json() instead of Inertia::render?
    The JSON response lacks the `X-Inertia` response header, so the client does not treat it as a page. It fires the cancelable `httpException` event (named `invalid` before v3) and, if no listener cancels it, shows the response in an error dialog. Plain JSON endpoints belong to fetch-style calls, not Inertia visits.

saying these in an interview costs you the question

  • Inertia::render returns JSON the frontend then fetches from a separate API route.
  • The first page load also returns bare JSON and the client builds the HTML.
  • The page object only needs component and props; url and version are optional.
  • Inertia 3 still reads the initial page from a data-page attribute on the root div.
  • Inertia switches to JSON because the client sends Accept: application/json.