I have been writing Inertia + Laravel + React for forty-plus projects, and the same hook always ended up in the codebase: a custom useFetchSomething that mapped 422s onto a form, tracked processing, and tried to remember to cancel itself. Inertia v3 just shipped that hook. It also threw out a few dependencies I had stopped noticing I was carrying.
useHttp is the hook you kept rewriting
Every Inertia + React app I have shipped has the same shaped utility somewhere in resources/js. It wraps fetch, sets a processing flag, maps 422 responses onto an errors object, holds an AbortController for the cancel I will inevitably forget, and exposes a progress value if anyone uploads files. It is the same hook in every codebase, with subtle differences nobody documents. v3 ships it.
useHttp gives you the form-shaped ergonomics of useForm, but for requests that should not trigger an Inertia page visit. Validation errors are mapped from 422 automatically. processing flips for the duration of the request. progress reports upload percentage. cancel() aborts. withPrecognition() plugs into Laravel Precognition for live validation. You can pass a key as the first argument and the data persists across browser history. None of this is novel. All of it is finally official.
The split is now honest. Use router for full page visits. Use useForm for forms that submit to an Inertia endpoint. Use useHttp for everything else: search inputs that should not navigate, like buttons, autocomplete, dashboard widgets that poll. The boundary between Inertia-the-router and Inertia-the-data-layer used to require taste. v3 hands you the third primitive.
Optimistic updates, with the rollback you would forget to write
Optimistic updates are the kind of pattern that looks easy on a whiteboard and turn into thirty lines once you are honest about failure. State A becomes state B in the UI, the request fires, the request fails, state B has to revert to state A, except a colleague clicked something else in the meantime and now state B is somebody else's intent. I have shipped that bug. More than once.
v3 makes the rollback the framework's problem. useHttp(...).optimistic(...) takes a function that returns the optimistic state, applies it immediately, and reverts cleanly if the request fails. The same primitive is available on useForm submissions and on router-driven visits, so the pattern is consistent across the whole visit hierarchy.
This alone is worth the upgrade for any list-heavy or form-heavy interface. Like buttons, drag-to-reorder, inline edits, the cluster of micro-interactions that make a UI feel modern. They have all been one-liners away from being correct. Now they are.
Axios is finally gone
Axios has shipped in every Inertia install since the beginning, and almost nobody used it directly. It sat under the router, doing the XHR work, exposing an interceptor API that maybe one in twenty teams ever called. v3 removes it. The router uses a built-in XHR client with the same interceptor surface, and your package.json is one line shorter.
qs is gone too, replaced with an internal serialiser. lodash-es got swapped for es-toolkit, which does the same job with smaller, tree-shakeable output. None of these are headline features. They are the kind of thing you only notice when your bundle gets a touch lighter and npm audit goes quiet for a week.
Between router for visits, useForm for forms, and useHttp for everything else, there is nothing left for Axios to do. The library has finally drawn its own boundary. If you still need Axios, install it directly. Most apps will not.
The Vite plugin earns the upgrade by itself
Setting up Inertia + React + SSR has historically meant editing five files in concert: vite.config.ts, app.tsx, ssr.tsx, the Laravel server entry, and a Node script to run the SSR bundle in dev. Forget one and the whole thing fails silently. I have onboarded two engineers onto Inertia projects this year, and SSR is the part that took us a full afternoon each time.
@inertiajs/vite is the new official plugin. It does automatic page resolution by convention. It wires SSR for you. It runs the SSR server in-process during dev, so you no longer need a side Node process to see SSR locally. The five-file dance becomes one plugin entry plus an optional callback.
The boot file shrinks accordingly. What used to be twenty-odd lines of createInertiaApp plumbing collapses into the plugin's defaults. You override only what is actually different about your project. That is roughly the entire promise of a good convention: you write the parts that are yours, and the framework writes the parts that are everyone's.
Instant visits, and what 'instant' actually means now
v3 swaps the visited component into place before the server's response arrives. The skeleton, the layout, the navigation all shift to the new page immediately, and the data fills in when it lands. It is a small change in router behaviour and a large change in how the app feels.
Network speed is rarely the user's bottleneck. The 80ms it takes a healthy Forge-hosted Laravel app to respond is not the problem. The 200ms of rendering blank space while waiting for that response is the problem. Instant visits remove that gap. Pair them with prefetched links on hover, and a normal monolith feels like a native app.
You will need to think a little about loading states for the data that has not arrived yet. v3 gives you reloading slot props on deferred components for exactly this. The trade is fair: a few skeleton shimmers in exchange for a UI that never feels like it stalled.
lazy() became optional(), and other honesties
Inertia::lazy() is now Inertia::optional(). Same behaviour, honest name. The prop is not lazy in any meaningful sense; it is optional, included only when the client asks for it via partial reload. Years of explaining that distinction in code review are now somebody else's problem.
The events got the same treatment. invalid is now httpException. exception is now networkError. They fire on what their names suggest, which is a low bar that the previous names actively failed to clear. Per-visit callbacks gained matching onHttpException and onNetworkError siblings.
The future config namespace is gone because the future arrived: preserveEqualProps, the script-tag-as-JSON initial page, the dialog-based error modal are all defaults now. The inertia head attribute is now data-inertia, which the HTML spec was always going to want anyway. None of this is interesting on its own. Together it reads like a library that has decided to stop apologising for its own past.
The version bumps are the real upgrade tax
PHP 8.2+. Laravel 11+. React 19. Svelte 5 with runes if you are over there. The toolchain is ES2022, ESM only. If you are current on all of these, the upgrade is largely a composer require and a few find-and-replace passes. If you are still on Laravel 10 or React 18, this is the nudge.
There is one breaking change in React land that will catch you. Arrow-function layout components no longer assign directly. Dashboard.layout = Layout stops working. You wrap it in an array: Dashboard.layout = [Layout]. The codemod handles most of these, but if you have a clever wrapper around Layout somewhere, expect to read it carefully.
None of these are gates. They are the cost of working with a library that has decided to stop carrying old runtimes for sentimental reasons. I would rather pay that cost once than have it spread thinly over the next two years of patch releases.
Removing things is a feature. Adding useHttp was the one thing worth adding. v3 got both right.