Inertia 3 support and full protocol coverage
5.0.0 (2026-08-09)
@adonisjs/inertia v5 moves the adapter onto Inertia 3, implements the v3 protocol
surface on the server (once props, rescued deferred props, keyed/directional merges,
infinite scroll, flash, shared-props tracking), and extends the typed client wrappers so
routes, request bodies, query strings, validation errors and instant visits are all checked
at compile time.
Consolidated from 5.0.0-next.0 → 5.0.0-next.3.
💥 Breaking changes
1. Inertia 3 client is now required
The adapter targets the @inertiajs/* v3 line. Client peer dependencies moved to
^3.4.0, which requires React 19 / Vue 3.5.
- Initial page data is emitted inside a
<script type="application/json">element instead
of adata-pageattribute on the root element. Handled by the@inertiaEdge tag — no
change needed unless you hand-wrote the payload. clearHistory/encryptHistoryare omitted from the page object unlesstrue.- Inertia 3's own client-side breaking changes apply to your app code:
inertia:invalid/
inertia:exceptionwere renamed toinertia:httpException/inertia:networkError;
router.cancel()→router.cancelAll(); the progress-bar exports were removed; React
layouts can no longer be arrow functions; Axios /qs/lodash-esare no longer bundled.
2. The @adonisjs/inertia/vite plugin has been removed
SSR is driven by @adonisjs/vite v6's serverEntrypoints. The @adonisjs/inertia/vite
export no longer exists and the ssr.bundle config option was removed — ssr.entrypoint is
the single source of truth, and that file must also be listed under serverEntrypoints so it
gets bundled for production.
Migration
-
Upgrade the packages:
npm i @adonisjs/inertia@latest @adonisjs/vite@latest npm i @inertiajs/react@^3.4.0 # or @inertiajs/vue3@^3.4.0 -
Move SSR to
serverEntrypointsinvite.config.ts:import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import adonisjs from '@adonisjs/vite/client' - import inertia from '@adonisjs/inertia/vite' export default defineConfig({ plugins: [ react(), - inertia({ ssr: { enabled: false, entrypoint: 'inertia/ssr.tsx' } }), - adonisjs({ entrypoints: ['inertia/app.tsx'], reload: ['resources/views/**/*.edge'] }), + adonisjs({ + entrypoints: ['inertia/app.tsx'], + serverEntrypoints: ['inertia/ssr.tsx'], + reload: ['resources/views/**/*.edge'], + }), ], }) -
Drop
ssr.bundlefromconfig/inertia.tsif present — keep onlyssr.entrypoint. -
Apply the Inertia 3 client changes in your frontend entrypoints per the
Inertia v3 upgrade guide.
✨ Server-side features
Once props
Computed once on the server, cached by the client across visits, and skipped on later
standard visits when the client reports a fresh cached value. Partial reloads always resolve
an explicitly requested once prop.
return inertia.render('dashboard', {
lookups: inertia.once(() => loadLookupTables()),
countries: inertia.once(() => fetchCountries(), { expiresIn: '2h' }),
plans: inertia.once(() => fetchPlans(), { key: 'globalPlans', fresh: true }),
}).once() is chainable on defer, optional, merge and deepMerge:
stats: inertia.defer(() => computeStats()).once(),
feed: inertia.merge(() => getFeed()).once({ expiresIn: '15m' }),Keyed and directional merges
merge() / deepMerge() gain chainable prepend(), append() and matchOn(). The page
object emits prependProps and matchPropsOn, and the X-Inertia-Reset request header is
honoured (reset props are emitted unlabeled so the client replaces instead of merging).
messages: inertia.merge(() => getOlderMessages()).prepend(),
users: inertia.merge(() => getUsers()).matchOn('id'),
settings: inertia.deepMerge(() => getSettings()).matchOn('list.id'),matchOn keys are typed from the merged items — flat array values only accept a field of
the item, while deep-merged values keep free-form dotted paths.
Infinite scroll
inertia.scroll() pairs with Inertia's InfiniteScroll component. The cursor is auto-derived
from a transformer paginator's metadata, or supplied through a provider callback for
cursor/custom sources.
return inertia.render('users/index', {
users: inertia.scroll(() => UserTransformer.paginate(rows, paginator.getMeta())),
})Chain .deferred() to exclude the first page from the initial load (optional props only) and
.matchOn() for keyed dedup. Typing is end-to-end via the Scroll<Item> client marker:
declaring a prop as Scroll<User> makes AsPageProps require inertia.scroll() for it.
Rescued deferred props
Deferred props can opt into graceful failure with { rescue: true }. When resolution throws
on a partial reload the prop is omitted (never null), its path is reported in the top-level
rescuedProps array, and the client's <Deferred> rescue slot renders — the response itself
never errors.
stats: inertia.defer(() => computeExpensiveStats(), { rescue: true }),
feed: inertia.defer(() => getFeed(), { rescue: true }).merge().once(),Errors are surfaced out of band through a static listener, defaulting to ctx.logger.error:
Inertia.onRescue((error, { prop, ctx }) => {
ctx.logger.error({ err: error }, `deferred prop "${prop}" failed`)
})The second defer() argument still accepts a group name.
First-class flash messages
An optional flash() hook on the middleware, mirroring share(). Its return is emitted as a
sibling of props — not merged into them — and the field is omitted entirely when no hook is
defined, so the default wire format is unchanged.
export default class InertiaMiddleware extends BaseInertiaMiddleware {
flash(ctx: HttpContext) {
return ctx.session.flashMessages.all()
}
}The hook is the inferable source for client typing via InferFlashData, bridged into
@inertiajs/core's flashDataType so page.flash, onFlash and router.flash() are typed:
declare module '@inertiajs/core' {
interface InertiaConfig {
flashDataType: InferFlashData<InertiaMiddleware>
}
}Shared props tracking
The page object emits the v3 sharedProps field listing the top-level keys registered via
share(), so the client can carry shared props over during instant visits. Registered keys
are reported as-is; the field is omitted when no shared keys exist.
Multiple validation errors per field
getValidationErrors accepts an opt-in { allMessages: true } mode that emits every field as
a uniform string[] (including under the error-bag key). Default first-message behaviour is
unchanged, and the overloads return the precise value type per mode.
middleware.getValidationErrors(ctx)
// { email: 'Invalid email' }
middleware.getValidationErrors(ctx, { allMessages: true })
// { email: ['Invalid email', 'Email already taken'] }Other
- Page props may now return
null(#89). - The
Inertiaclass is macroable — register methods withInertia.macro()/
Inertia.getter(), with theHttpContextaugmentation typed against the package entrypoint
so user-landdeclare module '@adonisjs/inertia'augmentations resolve (#102).
⚡ Client features
Route-aware useRouter
useRouter() now exposes get, post, put, patch and delete mirroring the upstream
signatures. Each verb only accepts the routes registered for it (or a direct href), and the
request body of post / put / patch is typed from the route's declared body — FormData
still accepted for uploads.
const router = useRouter()
router.post({ route: 'posts.store' }, { title: 'Hello Inertia' })Query strings in route mode
Link, Form and useRouter().visit accept a qs prop in route mode, typed from the route's
declared query types when present, and built through the client's urlFor.
<Link route="posts.index" qs={{ page: 2 }}>Page 2</Link>Method override on multi-method routes
Route mode accepts a method prop typed to the route's registered methods (excluding HEAD and
OPTIONS), so a PUT/PATCH pair is no longer locked to whichever was registered first.
Type-safe useHttp
useHttp accepts a route binding: the request body and response are inferred from the route
registry, submissions are bound to that endpoint, and AdonisJS validation errors are normalized
to Inertia's field-keyed error format on every request. Calls without a route keep the upstream
signatures.
const request = useHttp({ route: 'users.store' }, { email: 'virk@adonisjs.com' })
await request.submit()Correlated instant visits
Controllers record which page (or union of pages) a route renders, so on Link the component
prop is limited to those pages and pageProps follows the chosen page's declared props — no
blanket Partial, so a prop the page cannot render without stays required. Shared props are
omitted from the demand since the runtime carries them across instant visits.
<Link route="projects.show" routeParams={{ slug: project.slug }} instant
component="projects/show" pageProps={() => ({ project })}>
{project.title}
</Link>Vue wrappers
LinkandFormnow type the full upstream props surface (preserve-scroll,prefetch,
error-bag,transform, …) instead of forwarding them through untyped attrs, so templates
get checking and autocomplete.hrefalso accepts theUrlMethodPairobject form.- The
Formwrapper forwards its template ref to the inner form, making the programmatic API
(submit,reset,setError, …) reachable; aFormReftype is exported for typing refs.
React Form
The wrapper preserves the upstream form-data generic and accepts a TForm generic for typed
error keys, with new FormSlotProps and FormRef exports. Slot error types are prettified.
🐛 Fixes
- Do not read the Vite manifest for asset versioning in dev mode — a manifest left behind by a
previousnode ace buildcrashed every dev-mode request (#97). - Send the asset version on the version-mismatch (409) response, so the client can tell an
automatic version change from an explicit redirect and defer forced reloads on background
requests until the next user-initiated visit. - Deferred mergeable props no longer execute their compute on standard visits — this caused
duplicated items on first paint and the compute running twice per visit..deepMerge()on
defer()also now exists at runtime, as documented. - Preserve route body types in
Form(#104, thanks @RomainLanz). useRouter().visitnarrows an undefinedhrefinstead of visitingundefined.- Validation errors returned to
useHttpare normalized to Inertia's error shape.
📌 Scope notes
- Prop resolution stays top-level only — nested / dot-notation deferred, merge and partial
reload keys are deliberately out of scope (dotted top-level keys remain the escape hatch). - Inertia DevTools support is not implemented yet and is planned for a follow-up release.
What's Changed
- feat!: replace Inertia Vite plugin with @adonisjs/vite serverEntrypoints by @thetutlage in #88
- Preserve route body types in Form by @RomainLanz in #104
New Contributors
- @RomainLanz made their first contribution in #104
Full Changelog: v4.2.0...v5.0.0