Upgrades Inertia.js from v2 to v3 on both sides — the major deliberately
left out of #764.
| Package | From | To |
| --- | --- | --- |
| `inertiajs/inertia-laravel` | 2.0.24 | **3.3.1** |
| `@inertiajs/react` | 2.3.17 | **3.6.1** |
| `axios` | transitive | **direct dep** (1.19.0) |
## Why axios becomes a direct dependency
The app imports `axios` in 18 runtime files, but it was never declared —
it arrived transitively through `@inertiajs/core` v2. v3 drops axios in
favour of its own XHR client, so without this the whole app would fail
to resolve the import.
Nothing relied on Inertia configuring axios: v2 never touched
`axios.defaults`, and there are no interceptors. `^1.19.0` also
satisfies core's optional peer range (`^1.15.2`).
## Breaking changes swept
Every v2→v3 breaking change from the [official upgrade
guide](https://inertiajs.com/upgrade-guide) was checked against the
codebase:
| Change | Call sites |
| --- | --- |
| `<title inertia>` → `<title data-inertia>` | fixed (`app.blade.php`) |
| `testing.page_paths/page_extensions` → `pages.paths/extensions` |
fixed (`config/inertia.php`) |
| `ComponentResolver` no longer accepts `Promise<{default}>` | fixed
(`app.tsx`, `ssr.tsx`) |
| `router.on('invalid'/'exception')` → `httpException`/`networkError` |
none — only `'navigate'` is used |
| `router.cancel()` → `cancelAll()` | none |
| `Inertia::lazy()` → `optional()` | none |
| `future` options block | none |
| `hideProgress`/`revealProgress` | none |
| arrow-fn `.layout =` → `[Layout]` | none — no page sets `.layout` |
| deprecated testing traits (`Has`/`Matching`/`Debugging`) | none |
| `qs` / `lodash-es` imports | none |
React 19.2.4, Laravel 13 and PHP 8.4 already satisfy v3's raised floors.
## DevTools kept opt-in
v3 adds a DevTools recorder that writes every request's page props to
`storage/inertia-devtools/` and **defaults to on in local**. On this app
that produced 38 dumps, the largest ~1 MB, holding whole transaction and
balance payloads — redaction only covers credential-shaped keys. It is
gitignored by the package, so it never reaches the repo, but for a
privacy-first product it shouldn't be the default. Gated behind
`INERTIA_DEVTOOLS_ENABLED` (set it to `true` in your own `.env` when you
need it).
## Deliberate behaviour change: `<Deferred>` on partial reloads
v3 no longer resets `<Deferred>` to its `fallback` during a partial
reload — existing content stays visible while fresh data loads. This
affects the dashboard (`onBalanceUpdated`) and account detail
(`handleTransactionCreated`).
**Kept as-is, on purpose.** Two reasons:
1. `dashboard.tsx:131-133` already carried a workaround for exactly this
v2 behaviour — it requests an unrelated cheap prop purely so the
deferred prop isn't refetched into a skeleton. The team already treated
the skeleton flash as a problem and v3 fixes it upstream, so restoring
it via the new `reloading` slot prop would undo an improvement. That
workaround is now redundant and can be simplified in a follow-up.
2. The "user can't tell it worked and re-submits" risk doesn't apply:
`edit-transaction-dialog.tsx:390` fires `toast.success('Transaction
created successfully')` *before* the reload, so the confirmation is
explicit and independent of the list.
## Verification
Full CI set, locally:
- `bun run build` — green
- `bun run test` — **353/353**
- `bun run types` — zero new errors (advisory step; pre-existing backlog
unchanged)
- `bun run format`, `bun run lint`, `bun run dry` — green
- `vendor/bin/pint --test`, `vendor/bin/phpstan analyse` — green
- `php artisan crap --base=origin/main` — 0 methods above complexity 10
- `pest --exclude-testsuite=Browser,Performance` — **2086/2088**
- `pest --testsuite=Browser` — **132/133**
Both test failures are pre-existing and reproduce identically on `main`
with Inertia v2 — I ran each one there to confirm:
- `DashboardTest::dashboard top categories roll child spending up into
the parent` — 409, the known local Inertia asset-version artifact
- `BudgetsFeatureNavigationTest::user cannot access another users
budget` — `Unknown column 'category_id'` on `budgets`, a factory/schema
mismatch
## Browser QA
Real browser sweep against the production build over dashboard,
transactions, accounts, budgets, settings and billing: **0 console
errors, 0 uncaught exceptions, 0 failed requests**. Page titles apply,
deferred props resolve, sidebar `<Link>` navigation stays client-side
with no document reloads, back/forward restores from Inertia history,
and a profile form submit persisted correctly.
## SSR: measured, left alone
v3 routes SSR through the Vite dev server (`/__inertia_ssr`) whenever
Vite is running hot, and we don't use the `@inertiajs/vite` plugin — so
in theory local dev makes a doomed POST per render. Measured it rather
than guessed: with `bun run dev` up, page loads are **16 ms** and the
failed attempt is a connection-refused costing ~0.6 ms, then it falls
back to client rendering. Dev mode renders correctly (React mounts, 0
console errors). Not worth a config knob.
Production is untouched — `Vite::isRunningHot()` is false there, so it
uses the real SSR server from `docker/supervisor/supervisord.conf`
exactly as on v2.
## Notes
- Wayfinder needed nothing: #764 already brought it to v0.1.21 (latest),
and `@laravel/vite-plugin-wayfinder` is on 0.1.7 (latest).
- Pre-existing bug both reviews surfaced, left for its own PR:
`auth/register.tsx:40` passes an `async` handler to `<Form onBefore>`,
which Inertia never awaits (same in v2 and v3), so
`transactionSyncService.clearAll()` is fire-and-forget on registration.
- `config/inertia.php` is hand-patched rather than republished. The v3
stub is ~190 lines; the unset keys resolve to package defaults via
`mergeConfigFrom`. Adopting the whole stub is a separate, reviewable
change.
- Remaining "Inertia v2" strings live inside Boost-generated blocks
(`CLAUDE.md`'s guidelines block and the vendored `SKILL.md` copies).
Hand-edits there get overwritten, so they should come from a Boost
regeneration. The three hand-written references are updated here.
## Demo
https://github.com/user-attachments/assets/3f0e7f6d-621a-4c3a-a2a0-68473ec59f10
<!-- PLACEHOLDER: drag the QA video in here -->