Skip to content

Inertia 3 support and full protocol coverage

Choose a tag to compare

@github-actions github-actions released this 09 Aug 07:30
· 3 commits to 5.x since this release
Immutable release. Only release title and notes can be modified.

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.05.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 a data-page attribute on the root element. Handled by the @inertia Edge tag — no
    change needed unless you hand-wrote the payload.
  • clearHistory / encryptHistory are omitted from the page object unless true.
  • Inertia 3's own client-side breaking changes apply to your app code: inertia:invalid /
    inertia:exception were renamed to inertia:httpException / inertia:networkError;
    router.cancel()router.cancelAll(); the progress-bar exports were removed; React
    layouts can no longer be arrow functions; Axios / qs / lodash-es are 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

  1. Upgrade the packages:

    npm i @adonisjs/inertia@latest @adonisjs/vite@latest
    npm i @inertiajs/react@^3.4.0   # or @inertiajs/vue3@^3.4.0
  2. Move SSR to serverEntrypoints in vite.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'],
    +     }),
        ],
      })
  3. Drop ssr.bundle from config/inertia.ts if present — keep only ssr.entrypoint.

  4. 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 Inertia class is macroable — register methods with Inertia.macro() /
    Inertia.getter(), with the HttpContext augmentation typed against the package entrypoint
    so user-land declare 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

  • Link and Form now 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. href also accepts the UrlMethodPair object form.
  • The Form wrapper forwards its template ref to the inner form, making the programmatic API
    (submit, reset, setError, …) reachable; a FormRef type 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
    previous node ace build crashed 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().visit narrows an undefined href instead of visiting undefined.
  • Validation errors returned to useHttp are 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

Full Changelog: v4.2.0...v5.0.0