--- url: /guide/asset-versioning.md --- # Asset Versioning One common challenge when building single-page apps is refreshing site assets when they've been changed. Thankfully, Inertia makes this easy by optionally tracking the current version of your site assets. When an asset changes, Inertia will automatically make a full page visit instead of a XHR visit on the next request. ## Configuration To enable automatic asset refreshing, you need to tell Inertia the current version of your assets using the `InertiaRails.configure` method and setting the `config.version` property. This can be any arbitrary string (letters, numbers, or a file hash), as long as it changes when your assets have been updated. ```ruby InertiaRails.configure do |config| config.version = ViteRuby.digest # or any other versioning method end # You can also use lazy evaluation InertiaRails.configure do |config| config.version = lambda { ViteRuby.digest } end ``` ## Cache Busting Asset refreshing in Inertia works on the assumption that a hard page visit will trigger your assets to reload. However, Inertia doesn't actually do anything to force this. Typically this is done with some form of cache busting. For example, appending a version query parameter to the end of your asset URLs. ## Manual Refreshing If you want to take asset refreshing into your own control, you can set the version to a fixed value. This disables Inertia's automatic asset versioning. For example, if you want to notify users when a new version of your frontend is available, you can still expose the actual asset version to the frontend by including it as [shared data](/guide/shared-data). ```ruby # config/initializers/inertia_rails.rb InertiaRails.configure do |config| # Disable automatic asset versioning config.version = nil end # app/controllers/application_controller.rb class ApplicationController < ActionController::Base inertia_share version: -> { ViteRuby.digest } end ``` On the frontend, you can watch the `version` property and show a notification when a new version is detected. --- --- url: /guide/authentication.md --- # Authentication One of the benefits of using Inertia is that you don't need a special authentication system such as OAuth to connect to your data provider (API). Also, since your data is provided via your controllers, and housed on the same domain as your JavaScript components, you don't have to worry about setting up CORS. Rather, when using Inertia, you can simply use whatever authentication system you like, such as solutions based on Rails' built-in `has_secure_password` method, or gems like [Devise](https://github.com/heartcombo/devise), [Sorcery](https://github.com/Sorcery/sorcery), [Authentication Zero](https://github.com/lazaronixon/authentication-zero), etc. --- --- url: /guide/authorization.md --- # Authorization When using Inertia, authorization is best handled server-side in your application's authorization policies. However, you may be wondering how to perform checks against your authorization policies from within your Inertia page components since you won't have access to your framework's server-side helpers. The simplest approach to solving this problem is to pass the results of your authorization checks as props to your page components. Here's an example of how you might do this in a Rails controller using the [Action Policy](https://github.com/palkan/action_policy) gem: ```ruby class UsersController < ApplicationController def index render inertia: { can: { create_user: allowed_to?(:create, User) }, users: User.all.map do |user| user.as_json( only: [:id, :first_name, :last_name, :email] ).merge( can: { edit_user: allowed_to?(:edit, user) } ) end } end end ``` --- --- url: /awesome.md --- # Awesome Inertia Rails A curated list of community resources, tools, and content for Inertia Rails. ## Starter kits (alphabetical) * [GrowthX Starter Kit](https://github.com/growthxai/starter) - Rails 8 + React 19 + Inertia.js + Shadcn/UI Starter Kit from GrowthX.ai * [Inertia Rails Starter Kits](https://inertia-rails.dev/guide/starter-kits) — Official starter kits for React, Vue, and Svelte (TypeScript, shadcn/ui) * [Kaze](https://github.com/gtkvn/kaze) — Rails authentication scaffolding (Hotwire/React/Vue) * [Kickstart](https://github.com/alec-c4/kickstart) — Rails application starter kits, Inertia-based (React, Svelte, Vue) and classic (API, esbuild and importmaps) * [Svelte starter template](https://github.com/georgekettle/rails_svelte) — Svelte starter template (Svelte, shadcn/ui) ## Packages (alphabetical) ### Inertia-specific * [Alba::Inertia](https://github.com/skryukov/alba-inertia) — Seamless integration between Alba serializers and Inertia Rails * [Inertia Modal](https://github.com/inertiaui/modal) — Open any route in a Modal or Slideover without having to change anything about your existing routes or controllers (React, Vue) * [Inertia X](https://github.com/buhrmi/inertiax) — Svelte-only fork of Inertia with additional features (Svelte) * [InertiaBuilder](https://github.com/rodrigotavio91/inertia-builder) — JBuilder-like DSL for declaring Inertia.js props * [InertiaCable](https://github.com/cole-robertson/inertia-cable) — Real-time prop updates for Inertia.js Rails apps using ActionCable * [InertiaI18n](https://github.com/alec-c4/inertia_i18n) — Translation management bridging Rails YAML and frontend i18next JSON * [useInertiaForm](https://github.com/aviemet/useInertiaForm) — Direct replacement of Inertia's useForm hook with support for nested forms (React) ### General * [JS From Routes](https://github.com/ElMassimo/js_from_routes) — Generate path helpers and API methods from your Rails routes * [JsRoutes](https://github.com/railsware/js-routes) — Brings Rails named routes to JavaScript * [Typelizer](https://github.com/skryukov/typelizer) — A TypeScript type generator for Ruby serializers * [types\_from\_serializers](https://github.com/ElMassimo/types_from_serializers) — Generate TypeScript interfaces from your JSON serializers ## Articles (newest first) * [Optimistic UI in Rails with optimism... and Inertia](https://evilmartians.com/chronicles/optimistic-ui-in-rails-with-optimism-and-inertia) — Deep dive by Svyatoslav Kryukov (2026) * [Redprints CFP: an open source CFP management app built with Rails + Inertia.js](https://evilmartians.com/chronicles/redprints-cfp-open-source-cfp-management-app-build-with-rails-and-inertia-js) — Deep dive by Vladimir Dementyev (2025) * [Simplicity, vanished?! Solving the mystery with Inertia.js + Rails](https://evilmartians.com/chronicles/simplicity-vanished-solving-the-mystery-with-inertia-js-and-rails) — Deep dive by Svyatoslav Kryukov (2025) * [Building Filters with Inertia.js and Rails: A Clean Approach](https://pedro.switchdreams.com.br/inertiajs/2025/06/03/filters-with-inertia-and-rails/) — Guide by Pedro Duarte (2025) * [How to Build a Twitter Clone with Rails 8 Inertia and React](https://robrace.dev/blog/build-a-twitter-clone-with-rails-inertia-and-react) — Tutorial by Rob Race (2025) * [How to Handle Bundle Size in Inertia.js](https://pedro.switchdreams.com.br/inertiajs/2025/03/21/handle-bundle-size-inertiajs) — Guide by Pedro Duarte (2025) * [How We Fell Out of Love with Next.js and Back in Love with Ruby on Rails & Inertia.js](https://hardcover.app/blog/part-1-how-we-fell-out-of-love-with-next-js-and-back-in-love-with-ruby-on-rails-inertia-js) — Deep dive by Adam Fortuna (2025) * [Building an InertiaJS app with Rails](https://avohq.io/blog/inertia-js-with-rails) — Tutorial by Exequiel Rozas (2025) * [Inertial Rails project setup to use code generated from v0 (ShadcnUI, TailwindCSS4, React, TypeScript) and deploy with Kamal](https://tuyenhx.com/blog/inertia-rails-shadcn-typescript-ssr-en/) — Guide by Tom Ho (2025) * [Keeping Rails cool: the modern frontend toolkit](https://evilmartians.com/chronicles/keeping-rails-cool-the-modern-frontend-toolkit) — Deep dive by Irina Nazarova (2024) * [Inertia.js in Rails: a new era of effortless integration](https://evilmartians.com/chronicles/inertiajs-in-rails-a-new-era-of-effortless-integration) — Deep dive by Svyatoslav Kryukov (2024) ## Videos and talks (newest first) * [Inertia Rails Workshop](https://www.youtube.com/watch?v=XBZcLD5xcPY) — SF Ruby Conference 2025 workshop by Brandon Shar, Svyatoslav Kryukov, and Brian Knoles ([demo app](https://github.com/inertia-rails/inertia-workshop-demo)) * [Optimistic Drawer UI with Inertia.js and Rails](https://www.youtube.com/watch?v=WOd3uNlW37I) — by Brian Knoles (2025) * [Optimistic UI Updates with Inertia.js and Rails](https://www.youtube.com/watch?v=B8D195yQX04) — by Brian Knoles (2025) * [The Best of Both Worlds: InertiaJS + React + Rails](https://www.youtube.com/watch?v=MHqITeJGci0) — by Colleen Schnettler (2025) * [Tropical on Rails 2025: Defying Front-End Inertia](https://www.youtube.com/watch?v=uLFItMoF_wA) — Conference talk by Svyatoslav Kryukov (2025) * [Ken Greeff's YouTube channel](https://www.youtube.com/@kengreeff/search?query=inertia) — Fresh Inertia Rails content (2025) * [InertiaJS on Rails](https://www.youtube.com/watch?v=03EjkPaCHEI\&list=PLRxuhjCzzcWj4MUjDCC9TCP_ZfcRL0I1s) — YouTube course by Brandon Shar (2021) * [RailsConf 2021: Inertia.js on Rails Lightning Talk](https://www.youtube.com/watch?v=-JT1RF-IhKs) — Conference talk by Brandon Shar (2021) ## Example applications (alphabetical) ### Open source * [Code Basics](https://code-basics.com) — Free online programming courses. **Code available on [GitHub](https://github.com/hexlet-basics/hexlet-basics)** * [Redprints CFP](https://github.com/evilmartians/redprints-cfp) — Open source CFP management app built with Rails and Inertia.js ### Demo * [Ruby on Rails/Vue](https://github.com/ledermann/pingcrm) by Georg Ledermann * [Ruby on Rails/Vue SSR/Vite](https://github.com/ElMassimo/pingcrm-vite) by Máximo Mussini ### Production * [Calm Companies](https://calmcompanies.club) — Get weekly emails when companies with a great work culture are hiring * [Clipflow](https://www.clipflow.co) — Project management for content creators * [Crevio](https://crevio.co) — All-In-One creator store * [Hardcover](https://hardcover.app) — A social network for book lovers * [OG Pilot](https://ogpilot.com) — A dynamic Open Graph image generator tool * [Switch Kanban](https://switchkanban.com.br) — Project management tool for software houses ## Other (alphabetical) * [Inertia Rails Skills](https://github.com/cole-robertson/inertia-rails-skills) — Agent skills and best practices for AI coding assistants working with Inertia Rails * [Inertia.js devtools](https://chromewebstore.google.com/detail/inertiajs-devtools/golilfffgehhabacoaoilfgjelagablo?hl=en) — Inertia.js page JSON in devtools panel ## Community * [X.com](https://x.com/inertiajs) — Official X account * [Discord](https://discord.gg/inertiajs) — Official Discord server * [Reddit](https://www.reddit.com/r/inertiajs) — Inertia.js subreddit *Please share your projects and resources with us!* --- --- url: /guide/cached-props.md --- # Cached Props @available\_since rails=3.21.0 Cached props use your server-side cache store to avoid recomputing expensive data on every request. When the cache is warm, the block is never evaluated — Inertia serves the pre-serialized JSON directly. > \[!NOTE] > To understand when to use cached props vs once props vs HTTP caching, see the [Caching](/guide/caching) guide. ## Creating Cached Props To create a cached prop, use the `InertiaRails.cache` method. This method requires a cache key and a block that returns the prop data. ```ruby class DashboardController < ApplicationController def index render inertia: { stats: InertiaRails.cache('dashboard_stats', expires_in: 1.hour) { Stats.compute }, } end end ``` On the first request, the block is evaluated, serialized to JSON, and written to the cache. Subsequent requests serve the cached JSON without evaluating the block. ## Cache Keys Cache keys determine when cached data is invalidated. Inertia supports several key formats. ### String Keys The simplest form — a static string: ```ruby InertiaRails.cache('sidebar_nav') { NavigationItem.tree } ``` ### Active Record Objects Pass an Active Record object to derive the key from `cache_key_with_version`. The cache is automatically invalidated when the record is updated: ```ruby InertiaRails.cache(@post) { PostSerializer.render(@post) } # Cache key: "inertia_rails/posts/1-20260410120000" ``` ### Array Keys Pass an array to build a composite key: ```ruby InertiaRails.cache(['stats', current_user.id]) { Stats.for(current_user) } # Cache key: "inertia_rails/stats/42" ``` ## Cache Options You can pass `expires_in` and `race_condition_ttl` options to control cache behavior: ```ruby InertiaRails.cache('stats', expires_in: 1.hour) { Stats.compute } InertiaRails.cache('stats', expires_in: 1.hour, race_condition_ttl: 10.seconds) { Stats.compute } ``` ## Combining with Other Prop Types The `cache` option can be passed to [deferred](/guide/deferred-props) and [optional](/guide/partial-reloads#lazy-data-evaluation) props: ```ruby class DashboardController < ApplicationController def index render inertia: { # Deferred prop with caching feed: InertiaRails.defer(cache: { key: 'feed', expires_in: 5.minutes }, group: 'feed') { current_user.feed }, # Optional prop with caching categories: InertiaRails.optional(cache: @team) { @team.categories }, } end end ``` The `cache` option accepts the same key formats as `InertiaRails.cache`: strings, Active Record objects, arrays, and hashes with options. ```ruby InertiaRails.defer(cache: { key: 'feed', expires_in: 5.minutes }) { current_user.feed } ``` ## Cache Store By default, Inertia uses `Rails.cache`. You can configure a different store via the [`cache_store`](/guide/configuration#cache_store) option. All cached prop keys are automatically prefixed with `inertia_rails/` to avoid collisions. For more information on configuring cache stores, cache key strategies, and expiration policies, see the [Rails low-level caching guide](https://guides.rubyonrails.org/caching_with_rails.html#low-level-caching-using-rails-cache). --- --- url: /guide/caching.md --- # Caching Inertia Rails offers several complementary strategies for avoiding redundant work. Each solves a different problem, and you can combine them. If you're new to caching in Rails, start with the [Rails caching guide](https://guides.rubyonrails.org/caching_with_rails.html). ## Choosing a Strategy | Strategy | What it saves | Where it lives | Best for | | ------------------------------------ | --------------------------- | ----------------------- | ----------------------------------------- | | [HTTP caching](#http-caching) | Entire response render | Browser / CDN | Pages tied to a single record's freshness | | [Cached props](#prop-level-caching) | Block evaluation | Server-side cache store | Expensive queries or computations | | [Once props](#once-props) | Bandwidth and serialization | Client memory | Large or rarely-changing shared data | | [SSR caching](#ssr-response-caching) | SSR render request | Server-side cache store | Avoiding redundant Node.js SSR calls | In short: **cached props skip computing**, **once props skip sending**, **HTTP caching skips rendering**, and **SSR caching skips the SSR request**. These strategies are independent. A prop can be both cached on the server and marked as once so the client doesn't re-request it. HTTP caching can wrap an entire response that contains cached props. SSR caching can be layered on top of any combination. ### Why only `defer` and `optional` support the `cache` option The `cache` option is available on [deferred](/guide/deferred-props) and [optional](/guide/partial-reloads#lazy-data-evaluation) props because these represent data that is loaded on demand — caching their result avoids re-evaluating expensive blocks on repeated requests. Other prop types don't need it: * **Once props** already skip evaluation when the client has the data. If the computation itself is expensive, use `InertiaRails.cache` directly in the block. * **Always props** are meant for cheap, frequently-changing data (flash messages, auth state). If the data is expensive enough to cache, it probably shouldn't be `always`. * **Merge and scroll props** describe how the client handles the data, not how the server computes it. If the underlying data is expensive, wrap it with `InertiaRails.cache` and pass the result. ## HTTP Caching Rails provides built-in HTTP caching via `stale?` and `fresh_when`. These work with Inertia responses, but require one adjustment: because the same URL returns HTML on the initial page load and JSON on subsequent Inertia visits, ETags must account for the request type. ### Differentiating ETags Use the `etag` method in your controller to include `request.inertia?` in the ETag calculation: ```ruby class ApplicationController < ActionController::Base etag { request.inertia? } end ``` This ensures that HTML and JSON responses for the same URL produce different ETags, preventing the browser from serving a stale cached response in the wrong format. ### Using `stale?` With the ETag differentiation in place, use `stale?` as you normally would in Rails: ```ruby class PostsController < ApplicationController def show @post = Post.find(params[:id]) if stale?(@post) render inertia: { post: @post.as_json } end end end ``` When the post hasn't changed, Rails returns a `304 Not Modified` response and skips rendering entirely. ### Using `fresh_when` For simpler cases where you don't need conditional logic: ```ruby class PostsController < ApplicationController def show @post = Post.find(params[:id]) fresh_when(@post) render inertia: { post: @post.as_json } end end ``` ## Prop-Level Caching Prop-level caching stores computed values in your Rails cache store, skipping expensive block evaluation on cache hits. See the [Cached props](/guide/cached-props) guide for full details. ```ruby class DashboardController < ApplicationController def index render inertia: { stats: InertiaRails.cache('dashboard_stats', expires_in: 1.hour) { Stats.compute }, feed: InertiaRails.defer(cache: { key: 'feed', expires_in: 5.minutes }) { current_user.feed }, } end end ``` ## Once Props Once props are remembered by the client and excluded from subsequent responses. This saves bandwidth for data that rarely changes, such as shared navigation or role lists. See the [Once props](/guide/once-props) guide for full details. ```ruby class ApplicationController < ActionController::Base inertia_share countries: InertiaRails.once { Country.all } end ``` ## SSR Response Caching When SSR is enabled, each page load sends a request to the Node.js server. SSR caching stores these responses so identical page data is only rendered once. See [SSR response caching](/guide/server-side-rendering#ssr-response-caching) for full details. ```ruby InertiaRails.configure do |config| config.ssr_cache = true end ``` --- --- url: /guide/client-side-setup.md --- # Client-Side Setup Once you have your [server-side framework configured](/guide/server-side-setup), you then need to setup your client-side framework. Inertia currently provides support for React, Vue, and Svelte. ## Prerequisites > \[!NOTE] > You can skip this step if you have already executed the [Rails generator](/guide/server-side-setup#rails-generator). Inertia requires your client-side framework and its corresponding Vite plugin to be installed and configured. You may skip this section if your application already has these set up. :::tabs key:frameworks \== Vue ```bash npm install vue @vitejs/plugin-vue ``` \== React ```bash npm install react react-dom @vitejs/plugin-react ``` \== Svelte ```bash npm install svelte @sveltejs/vite-plugin-svelte ``` ::: Then, add the framework plugin to your `vite.config.js` file. :::tabs key:frameworks \== Vue ```js import { defineConfig } from 'vite' import RubyPlugin from 'vite-plugin-ruby' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [RubyPlugin(), vue()], }) ``` \== React ```js import { defineConfig } from 'vite' import RubyPlugin from 'vite-plugin-ruby' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [RubyPlugin(), react()], }) ``` \== Svelte ```js import { defineConfig } from 'vite' import RubyPlugin from 'vite-plugin-ruby' import { svelte } from '@sveltejs/vite-plugin-svelte' export default defineConfig({ plugins: [RubyPlugin(), svelte()], }) ``` ::: For more information on configuring these plugins, consult [Vite Rails documentation](https://vite-ruby.netlify.app). ## Installation > \[!NOTE] > You can skip this step if you have already executed the [Rails generator](/guide/server-side-setup#rails-generator). ### Install dependencies @available\_since core=3.0.0 Install the Inertia client-side adapter and Vite plugin. > \[!NOTE] > The `@inertiajs/vite` plugin supports Vite 7 and Vite 8. :::tabs key:frameworks \== Vue ```bash npm install @inertiajs/vue3 @inertiajs/vite ``` \== React ```bash npm install @inertiajs/react @inertiajs/vite ``` \== Svelte ```bash npm install @inertiajs/svelte @inertiajs/vite ``` ::: ### Configure Vite @available\_since core=3.0.0 Add the Inertia plugin to your `vite.config.js` file. ```js import inertia from '@inertiajs/vite' import RubyPlugin from 'vite-plugin-ruby' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ RubyPlugin(), inertia(), // ... ], }) ``` ### Initialize the Inertia app Update your main JavaScript file to boot your Inertia app. The Vite plugin handles page resolution and mounting automatically, so a minimal entry point is all you need. :::tabs key:frameworks \== Vue ```js import { createInertiaApp } from '@inertiajs/vue3' createInertiaApp() ``` \== React ```js import { createInertiaApp } from '@inertiajs/react' createInertiaApp() ``` \== Svelte ```js import { createInertiaApp } from '@inertiajs/svelte' createInertiaApp() ``` ::: The plugin generates a default resolver that looks for pages in both `./pages` and `./Pages` directories, and the app mounts automatically. ### React Strict Mode @available\_since core=3.0.0 The React adapter supports enabling React's [Strict Mode](https://react.dev/reference/react/StrictMode) via the `strictMode` option. ```jsx createInertiaApp({ strictMode: true, // ... }) ``` ### Pages Shorthand @available\_since core=3.0.0 You may use the `pages` shorthand to customize which directory to search for page components. :::tabs key:frameworks \== Vue ```js import { createInertiaApp } from '@inertiajs/vue3' createInertiaApp({ pages: './AppPages', // ... }) ``` \== React ```js import { createInertiaApp } from '@inertiajs/react' createInertiaApp({ pages: './AppPages', // ... }) ``` \== Svelte ```js import { createInertiaApp } from '@inertiajs/svelte' createInertiaApp({ pages: './AppPages', // ... }) ``` ::: An object may also be provided for more control over how pages are resolved. You only need to include the options you wish to customize. ```js createInertiaApp({ pages: { path: './Pages', extension: '.tsx', lazy: true, transform: (name, page) => name.replace('/', '-'), }, }) ``` | Option | Description | | ----------- | -------------------------------------------------------------------------------------------------------------------- | | `path` | The directory to search for page components. | | `extension` | A string or array of file extensions (e.g., `'.tsx'` or `['.tsx', '.jsx']`). Defaults to your framework's extension. | | `lazy` | Whether to lazy-load page components. Defaults to `true`. See [code splitting](/guide/code-splitting). | | `transform` | A callback that receives the page name and page object, returning a transformed name. | ## Customizing the App Sometimes you may wish to customize the app instance, for example to register plugins, wrap with providers, or set context values. Pass the `withApp` callback to `createInertiaApp` to hook into the app before it renders. :::tabs key:frameworks \== Vue ```js import { createInertiaApp } from '@inertiajs/vue3' import { createI18n } from 'vue-i18n' const i18n = createI18n({ // ... }) createInertiaApp({ withApp(app) { app.use(i18n) }, }) ``` \== React ```jsx import { createInertiaApp } from '@inertiajs/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' const queryClient = new QueryClient() createInertiaApp({ withApp(app) { return {app} }, }) ``` \== Svelte ```js import { createInertiaApp } from '@inertiajs/svelte' createInertiaApp({ withApp(context) { context.set('theme', 'dark') }, }) ``` ::: A second `{ ssr }` argument is also available, allowing you to conditionally apply logic based on the rendering environment. :::tabs key:frameworks \== Vue ```js createInertiaApp({ withApp(app, { ssr }) { app.use(i18n) if (!ssr) { app.use(browserOnlyPlugin) } }, }) ``` \== React ```jsx createInertiaApp({ withApp(app, { ssr }) { if (!ssr) { return {app} } return app }, }) ``` \== Svelte ```js createInertiaApp({ withApp(context, { ssr }) { context.set('theme', 'dark') if (!ssr) { context.set('analytics', createAnalytics()) } }, }) ``` ::: ## Manual Setup If you prefer not to use the Vite plugin, you may provide the `resolve` and `setup` callbacks manually. The `resolve` callback tells Inertia how to load a page component and receives the component name and the full [page object](/guide/the-protocol). The `setup` callback initializes the client-side framework. > \[!NOTE] > A manual `setup` callback prevents the Vite plugin from auto-generating [SSR](/guide/server-side-rendering) handling. You should create a [separate SSR entry point](/guide/server-side-rendering#ssr-entry-point) and update your app to use [client-side hydration](/guide/server-side-rendering#client-side-hydration). :::tabs key:frameworks \== Vue ```js import { createApp, h } from 'vue' import { createInertiaApp } from '@inertiajs/vue3' createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('../pages/**/*.vue') return pages[`../pages/${name}.vue`]() }, setup({ el, App, props, plugin }) { createApp({ render: () => h(App, props) }) .use(plugin) .mount(el) }, }) ``` \== React ```jsx import { createInertiaApp } from '@inertiajs/react' import { createRoot } from 'react-dom/client' createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('../pages/**/*.jsx') return pages[`../pages/${name}.jsx`]() }, setup({ el, App, props }) { createRoot(el).render() }, }) ``` \== Svelte ```js import { createInertiaApp } from '@inertiajs/svelte' import { mount } from 'svelte' createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('../pages/**/*.svelte') return pages[`../pages/${name}.svelte`]() }, setup({ el, App, props }) { mount(App, { target: el, props }) }, }) ``` ::: By default, page components are lazy-loaded, splitting each page into its own bundle. To eagerly bundle all pages into a single file instead, see the [code splitting](/guide/code-splitting) documentation. ## Configuring Defaults @available\_since core=2.2.11 You may pass a `defaults` object to `createInertiaApp()` to configure default settings for various features. You don't have to pass a default for every key, just the ones you want to tweak. ```js createInertiaApp({ defaults: { form: { recentlySuccessfulDuration: 5000, }, prefetch: { cacheFor: '1m', hoverDelay: 150, }, visitOptions: (href, options) => { return { headers: { ...options.headers, 'X-Custom-Header': 'value', }, } }, }, // ... }) ``` The `visitOptions` callback receives the target URL and the current visit options, and should return an object with any options you want to override. For more details on the available configuration options, see the [forms](/guide/forms#form-errors), [prefetching](/guide/prefetching), and [manual visits](/guide/manual-visits#global-visit-options) documentation. ### Updating Configuration at Runtime You may also update configuration values at runtime using the exported `config` instance. This is particularly useful when you need to adjust settings based on user preferences or application state. :::tabs key:frameworks \== Vue ```js import { config } from '@inertiajs/vue3' // Set a single value using dot notation... config.set('form.recentlySuccessfulDuration', 1000) config.set('prefetch.cacheFor', '5m') // Set multiple values at once... config.set({ 'form.recentlySuccessfulDuration': 1000, 'prefetch.cacheFor': '5m', }) // Get a configuration value... const duration = config.get('form.recentlySuccessfulDuration') ``` \== React ```js import { config } from '@inertiajs/react' // Set a single value using dot notation... config.set('form.recentlySuccessfulDuration', 1000) config.set('prefetch.cacheFor', '5m') // Set multiple values at once... config.set({ 'form.recentlySuccessfulDuration': 1000, 'prefetch.cacheFor': '5m', }) // Get a configuration value... const duration = config.get('form.recentlySuccessfulDuration') ``` \== Svelte ```js import { config } from '@inertiajs/svelte' // Set a single value using dot notation... config.set('form.recentlySuccessfulDuration', 1000) config.set('prefetch.cacheFor', '5m') // Set multiple values at once... config.set({ 'form.recentlySuccessfulDuration': 1000, 'prefetch.cacheFor': '5m', }) // Get a configuration value... const duration = config.get('form.recentlySuccessfulDuration') ``` ::: ## Defining a Root Element By default, Inertia assumes that your application's root template has a root element with an `id` of `app`. If your application's root element has a different `id`, you can provide it using the `id` property. ```js createInertiaApp({ id: 'my-app', // ... }) ``` If you change the `id` of the root element, be sure to update it [server-side](/guide/server-side-setup#root-template) as well. ## HTTP Client @available\_since core=3.0.0 Unlike Inertia 2 and earlier, Inertia 3 uses a built-in XHR client for all requests. No additional HTTP libraries like Axios are required. ### Using Axios You may provide the `axiosAdapter` as the `http` option when creating your Inertia app. This is useful when your application requires a custom Axios instance. ```js import { axiosAdapter } from '@inertiajs/core' createInertiaApp({ http: axiosAdapter(), // ... }) ``` A custom Axios instance may also be provided to the adapter. ```js import axios from 'axios' import { axiosAdapter } from '@inertiajs/core' const instance = axios.create({ // ... }) createInertiaApp({ http: axiosAdapter(instance), // ... }) ``` ### Interceptors The built-in XHR client supports interceptors for modifying requests, inspecting responses, or handling errors. These interceptors apply to all HTTP requests made by Inertia, including those from the router, `useForm`, `
`, and `useHttp`. :::tabs key:frameworks \== Vue ```js import { http } from '@inertiajs/vue3' const removeRequestHandler = http.onRequest((config) => { config.headers['X-Custom-Header'] = 'value' return config }) const removeResponseHandler = http.onResponse((response) => { console.log('Response status:', response.status) return response }) const removeErrorHandler = http.onError((error) => { console.error('Request failed:', error) }) // Remove a handler when it's no longer needed... removeRequestHandler() ``` \== React ```js import { http } from '@inertiajs/react' const removeRequestHandler = http.onRequest((config) => { config.headers['X-Custom-Header'] = 'value' return config }) const removeResponseHandler = http.onResponse((response) => { console.log('Response status:', response.status) return response }) const removeErrorHandler = http.onError((error) => { console.error('Request failed:', error) }) // Remove a handler when it's no longer needed... removeRequestHandler() ``` \== Svelte ```js import { http } from '@inertiajs/svelte' const removeRequestHandler = http.onRequest((config) => { config.headers['X-Custom-Header'] = 'value' return config }) const removeResponseHandler = http.onResponse((response) => { console.log('Response status:', response.status) return response }) const removeErrorHandler = http.onError((error) => { console.error('Request failed:', error) }) // Remove a handler when it's no longer needed... removeRequestHandler() ``` ::: Each `on*` method returns a cleanup function that removes the handler when called. Request handlers receive the request config and must return it (modified or not). Response handlers receive the response and must also return it. Handlers may be asynchronous. ### Custom HTTP Client For full control over how requests are made, you may provide a completely custom HTTP client via the `http` option. A custom client must implement the `request` method, which receives an `HttpRequestConfig` and returns a promise resolving to an `HttpResponse`. Review the [xhrHttpClient.ts](https://github.com/inertiajs/inertia/blob/3.x/packages/core/src/xhrHttpClient.ts) source for a reference implementation. --- --- url: /guide/code-splitting.md --- # Code Splitting By default, Inertia 3.x lazy-loads page components, splitting each page into its own bundle that is loaded on demand. This reduces the initial JavaScript bundle size but requires additional requests when visiting new pages. You may disable lazy loading to eagerly bundle all pages into a single file. Eager loading eliminates per-page requests but increases the initial bundle size. ## Vite Plugin @available\_since core=3.0.0 The `lazy` option in the `pages` shorthand controls how page components are loaded. It defaults to `true`. ```js createInertiaApp({ pages: { lazy: false, // Bundle all pages into a single file }, // ... }) ``` ## Manual Vite You may configure code splitting manually using Vite's `import.meta.glob()` function when not using the Inertia Vite plugin. Pass `{ eager: true }` to bundle all pages, or omit it to lazy-load them. :::tabs key:frameworks \== Vue ```js createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('../pages/**/*.vue') // [!code --:2] return pages[`../pages/${name}.vue`]() const pages = import.meta.glob('../pages/**/*.vue', { eager: true }) // [!code ++:2] return pages[`../pages/${name}.vue`] }, }) ``` \== React ```js createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('../pages/**/*.jsx') // [!code --:2] return pages[`../pages/${name}.jsx`]() const pages = import.meta.glob('../pages/**/*.jsx', { eager: true }) // [!code ++:2] return pages[`../pages/${name}.jsx`] }, }) ``` \== Svelte ```js createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('../pages/**/*.svelte') // [!code --:2] return pages[`../pages/${name}.svelte`]() const pages = import.meta.glob('../pages/**/*.svelte', { eager: true }) // [!code ++:2] return pages[`../pages/${name}.svelte`] }, }) ``` ::: ## Using Webpacker/Shakapacker To use code splitting with Webpack, you will first need to enable [dynamic imports](https://github.com/tc39/proposal-dynamic-import) via a Babel plugin. Let's install it now. ```bash npm install @babel/plugin-syntax-dynamic-import ``` Next, create a `.babelrc` file in your project with the following configuration: ```json { "plugins": ["@babel/plugin-syntax-dynamic-import"] } ``` Finally, update the `resolve` callback in your app's initialization code to use `import` instead of `require`. :::tabs key:frameworks \== Vue ```js resolve: name => require(`../pages/${name}`), // [!code --] resolve: name => import(`../pages/${name}`), // [!code ++] ``` \== React ```js resolve: name => require(`../pages/${name}`), // [!code --] resolve: name => import(`../pages/${name}`), // [!code ++] ``` \== Svelte ```js resolve: name => require(`../pages/${name}.svelte`), // [!code --] resolve: name => import(`../pages/${name}.svelte`), // [!code ++] ``` ::: You should also consider using cache busting to force browsers to load the latest version of your assets. To accomplish this, add the following configuration to your webpack configuration file. ```js output: { chunkFilename: 'js/[name].js?id=[chunkhash]', } ``` --- --- url: /guide/configuration.md --- # Configuration Inertia Rails can be configured globally or in a specific controller (and subclasses). ## Global Configuration Use the `InertiaRails.configure` method to set global configuration options. If using global configuration, we recommend you place the code inside an initializer: ```ruby # config/initializers/inertia.rb InertiaRails.configure do |config| # Example: force a full-reload if the deployed assets change. config.version = ViteRuby.digest end ``` The default configuration can be found [here](https://github.com/inertiajs/inertia-rails/blob/master/lib/inertia_rails/configuration.rb#L5). ## Local Configuration The `inertia_config` method allows you to override global settings in specific controllers. Use this method in your controllers to customize configuration for specific parts of your application: ```ruby class EventsController < ApplicationController inertia_config( version: "events-#{InertiaRails.configuration.version}", ssr_enabled: -> { action_name == "index" }, ) end ``` ## Setting Configuration via Environment Variables Inertia Rails supports setting any configuration option via environment variables out of the box. For each option in the configuration, you can set an environment variable prefixed with `INERTIA_` and the option name in uppercase. For example: `INERTIA_SSR_ENABLED`. **Boolean values** (like `INERTIA_DEEP_MERGE_SHARED_DATA` or `INERTIA_SSR_ENABLED`) are parsed from the strings `"true"` or `"false"` (case-sensitive). ## Configuration Options ### `component_path_resolver` **Default**: `->(path:, action:) { "#{path}/#{action}" }` Use `component_path_resolver` to customize component path resolution when [`default_render`](#default_render) config value is set to `true`. The value should be callable and will receive the `path` and `action` parameters, returning a string component path. See [Automatically determine component name](/guide/responses#automatically-determine-component-name). ### `prop_transformer` **Default**: `->(props:) { props }` Use `prop_transformer` to apply a transformation to your props before they're sent to the view. One use-case this enables is to work with `snake_case` props within Rails while working with `camelCase` in your view: ```ruby inertia_config( prop_transformer: lambda do |props:| props.deep_transform_keys { |key| key.to_s.camelize(:lower) } end ) ``` > \[!NOTE] > This controls the props provided by Inertia Rails but does not concern itself with props coming *into* Rails. You may want to add a global `before_action` to `ApplicationController`: ```ruby before_action :underscore_params # ... def underscore_params params.deep_transform_keys! { |key| key.to_s.underscore } end ``` ### `cache_store` **Default**: `nil` (falls back to `Rails.cache`) The cache store used for [cached props](/guide/cached-props) and [SSR response caching](/guide/server-side-rendering). Accepts any `ActiveSupport::Cache::Store` instance. ```ruby InertiaRails.configure do |config| config.cache_store = ActiveSupport::Cache::MemoryStore.new end ``` ### `deep_merge_shared_data` **Default**: `false` **ENV**: `INERTIA_DEEP_MERGE_SHARED_DATA` @available\_since rails=3.8.0 When enabled, props will be deep merged with shared data, combining hashes with the same keys instead of replacing them. ### `default_render` **Default**: `false` **ENV**: `INERTIA_DEFAULT_RENDER` Overrides Rails default rendering behavior to render using Inertia by default. ### `encrypt_history` **Default**: `false` **ENV**: `INERTIA_ENCRYPT_HISTORY` @available\_since rails=3.7.0 core=2.0.0 When enabled, you instruct Inertia to encrypt your app's history, it uses the browser's built-in [`crypto` api](https://developer.mozilla.org/en-US/docs/Web/API/Crypto) to encrypt the current page's data before pushing it to the history state. ### `ssr_enabled` *(experimental)* **Default**: `false` **ENV**: `INERTIA_SSR_ENABLED` @available\_since rails=3.6.0 core=2.0.0 Whether to use a JavaScript server to pre-render your JavaScript pages, allowing your visitors to receive fully rendered HTML when they first visit your application. Requires a JavaScript server to be available at `ssr_url`. [*Example*](https://github.com/ElMassimo/inertia-rails-ssr-template) ### `ssr_cache` **Default**: `nil` (disabled) @available\_since rails=3.21.0 Cache SSR responses to avoid redundant Node.js render requests for identical page data. Accepts `true`, `false`/`nil`, or a Hash of `Rails.cache.fetch` options (e.g. `{ expires_in: 1.hour }`). Lambdas are supported and evaluated in the controller context. Can be overridden per render call. See [SSR response caching](/guide/server-side-rendering#ssr-response-caching) for usage examples. ### `ssr_url` *(experimental)* **Default**: `nil` (auto-detects from Vite dev server, falls back to `http://localhost:13714/render`) **ENV**: `INERTIA_SSR_URL` The URL of the SSR server. When `nil`, auto-detects from the Vite dev server. > \[!NOTE] > Inertia SSR expects the server to respond at `/render` (production) or `/__inertia_ssr` (dev). If you set a server address, `/render` is appended automatically unless the URL already ends with `/render` or `/__inertia_ssr`. ### `version` *(recommended)* **Default**: `nil` **ENV**: `INERTIA_VERSION` This allows Inertia to detect if the app running in the client is oudated, forcing a full page visit instead of an XHR visit on the next request. See [assets versioning](/guide/asset-versioning). ### `always_include_errors_hash` **Default**: `nil` **ENV**: `INERTIA_ALWAYS_INCLUDE_ERRORS_HASH` @available\_since rails=3.11.0 Whether to include an empty `errors` hash in the props when no validation errors are present. When set to `true`, an empty `errors: {}` object will always be included in Inertia responses. When set to `false`, the `errors` key will be omitted when there are no errors. The default value `nil` currently behaves like `false` but shows a deprecation warning. The default value will be changed to `true` in the next major version. ### `flash_keys` **Default**: `%i[notice alert]` @available\_since rails=3.17.0 core=2.3.3 Specifies which Rails flash keys are exposed to the frontend. By default, only known-safe keys (`notice`, `alert`) are included. ```ruby InertiaRails.configure do |config| # Default: safe keys only config.flash_keys = %i[notice alert] # Add your custom keys config.flash_keys = %i[notice alert toast message] # Disable Rails flash integration (use only flash.inertia) config.flash_keys = nil end ``` See the [flash data documentation](/guide/flash-data) for more details on using flash with Inertia. ### `parent_controller` **Default**: `'::ApplicationController'` **ENV**: `INERTIA_PARENT_CONTROLLER` Specifies the base controller class for the internal `StaticController` used to render [Shorthand routes](/guide/routing#shorthand-routes). By default, Inertia Rails creates a `StaticController` that inherits from `ApplicationController`. You can use this option to specify a different base controller (for example, to include custom authentication, layout, or before actions). ### `root_dom_id` **Default**: `'app'` **ENV**: `INERTIA_ROOT_DOM_ID` @available\_since rails=3.15.0 Specifies the DOM element ID used for the root Inertia.js element. ```ruby InertiaRails.configure do |config| config.root_dom_id = 'inertia-app' end ``` > \[!NOTE] > Make sure your client-side Inertia setup uses the same ID when calling `createInertiaApp`. ### `use_script_element_for_initial_page` **Default**: `false` **ENV**: `INERTIA_USE_SCRIPT_ELEMENT_FOR_INITIAL_PAGE` @available\_since rails=3.15.0 core=2.2.20 When enabled the initial page data is rendered in a `
``` > \[!NOTE] > When using this option make sure your client-side Inertia setup is configured to read the page data from the ` ``` \== React ```jsx import { Deferred } from '@inertiajs/react' export default () => ( Loading...}> ) ``` \== Svelte ```svelte {#snippet fallback()}
Loading...
{/snippet} {#each permissions as permission} {/each}
``` ::: ## Multiple Deferred Props If you need to wait for multiple deferred props to become available, you can specify an array to the `data` prop. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Deferred } from '@inertiajs/react' export default () => ( Loading...}> ) ``` \== Svelte ```svelte {#snippet fallback()}
Loading...
{/snippet}
``` ::: ## Reloading Indicator @available\_since core=3.0.0 When deferred props are being reloaded via a partial reload, the `Deferred` component exposes a `reloading` boolean through its slot. This allows you to show a loading indicator while still displaying the previously loaded data. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Deferred } from '@inertiajs/react' export default () => ( Loading...}> {({ reloading }) => (
)}
) ``` \== Svelte ```svelte {#snippet fallback()}
Loading...
{/snippet} {#snippet children({ reloading })}
{#each permissions as permission} {/each}
{/snippet}
``` ::: The `reloading` prop is `false` on the initial load and becomes `true` whenever a partial reload is in progress for the deferred keys. It returns to `false` once the reload completes. ## Combining with Once Props @available\_since rails=3.15.0 core=2.2.20 You may pass the `once: true` argument to a deferred prop to ensure the data is resolved only once and remembered by the client across subsequent navigations. ```ruby class DashboardController < ApplicationController def index render inertia: { stats: InertiaRails.defer(once: true) { Stats.generate }, } end end ``` For more information on once props, see the [once props](/guide/once-props) documentation. ## Combining with Caching @available\_since rails=3.21.0 You may pass the `cache` option to a deferred prop to cache the resolved value on the server side. On cache hits, the block is not evaluated. ```ruby class DashboardController < ApplicationController def index render inertia: { feed: InertiaRails.defer(cache: { key: 'feed', expires_in: 5.minutes }, group: 'feed') { current_user.feed }, } end end ``` For more information on cache keys and options, see the [cached props](/guide/cached-props) documentation. --- --- url: /guide/demo-application.md --- # Demo Application The official Inertia.js v3 demo application is a comprehensive [Kitchen Sink](https://demo-v3.inertiajs.com) app built with Laravel and Vue. It includes a mini CRM with contacts, organizations, and notes, along with dedicated feature showcase pages covering forms, navigation, data loading, prefetching, state management, layouts, events, error handling, and more. You may find the source code on [GitHub](https://github.com/inertiajs/demo-v3). The demo is hosted on [Laravel Cloud](https://cloud.laravel.com) and the database is reset every midnight. Please be respectful when editing data. ## Inertia 1.x-2.x Demo App The previos demo app for Inertia.js is called [Ping CRM](https://demo.inertiajs.com). This application is built using Laravel and Vue. You can find the source code on [GitHub](https://github.com/inertiajs/pingcrm). In addition to the Vue version of Ping CRM, we also maintain a Svelte version of the application, which you can find [on GitHub](https://github.com/inertiajs/pingcrm-svelte). ### Third Party Beyond our official demo app, Ping CRM has also been translated into numerous different languages and frameworks. | Platform | Author | | :---------------------------------------------------------------------- | :---------------- | | [Ruby on Rails/Vue](https://github.com/ledermann/pingcrm) | Georg Ledermann | | [Ruby on Rails/Vue SSR/Vite](https://github.com/ElMassimo/pingcrm-vite) | Máximo Mussini | | [Clojure/React](https://github.com/prestancedesign/pingcrm-clojure) | Michaël Salihi | | [Echo/Vue](https://github.com/kohkimakimoto/pingcrm-echo) | Kohki Makimoto | | [Grails/Vue](https://github.com/matrei/pingcrm-grails) | Mattias Reichel | | [Laravel/React](https://github.com/Landish/pingcrm-react) | Lado Lomidze | | [Laravel/Mithril.js](https://github.com/tbreuss/pingcrm-mithril) | Thomas Breuss | | [Laravel/Svelte](https://github.com/zgabievi/pingcrm-svelte) | Zura Gabievi | | [Symfony/Vue](https://github.com/aleksblendwerk/pingcrm-symfony) | Aleks Seltenreich | | [Yii 2/Vue](https://github.com/tbreuss/pingcrm-yii2) | Thomas Breuss | --- --- url: /guide/error-handling.md --- # Error Handling ## Development One of the advantages to working with a robust server-side framework is the built-in exception handling you get for free. The challenge is, if you're making an XHR request (which Inertia does) and you hit a server-side error, you're typically left digging through the network tab in your browser's devtools to diagnose the problem. Inertia solves this issue by showing all non-Inertia responses in a modal. This means you get the same beautiful error-reporting you're accustomed to, even though you've made that request over XHR. ## Dialog element @available\_since core=2.2.13 By default, Inertia < 3.x displays error modals using a custom `
` overlay. However, you can opt-in to using the native HTML `` element instead, which provides built-in modal functionality including backdrop handling. To enable this, configure the `future.useDialogForErrorModal` option in your [application defaults](/guide/client-side-setup#configuring-defaults). ```js createInertiaApp({ // resolve, setup, etc. defaults: { future: { useDialogForErrorModal: true, }, }, }) ``` ## Production In production you will want to return a proper Inertia error response instead of relying on the modal-driven error reporting that is present during development. To accomplish this, use the `rescue_from` method in your `ApplicationController`. ```ruby class ApplicationController < ActionController::Base rescue_from StandardError, with: :inertia_error_page private def inertia_error_page(exception) raise exception if Rails.env.local? status = ActionDispatch::ExceptionWrapper.new(nil, exception).status_code render inertia: 'ErrorPage', props: { status: }, status: end end ``` Since `rescue_from` runs inside the controller, shared data defined via `inertia_share` is available automatically. ### Error Page Example You'll need to create the error page components referenced above. Here's an example you may use as a starting point. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx export default function ErrorPage({ status }) { const title = { 503: '503: Service Unavailable', 500: '500: Server Error', 404: '404: Page Not Found', 403: '403: Forbidden', }[status] const description = { 503: 'Sorry, we are doing some maintenance. Please check back soon.', 500: 'Whoops, something went wrong on our servers.', 404: 'Sorry, the page you are looking for could not be found.', 403: 'Sorry, you are forbidden from accessing this page.', }[status] return (

{title}

{description}
) } ``` \== Svelte ```svelte

{title[status]}

{description[status]}
``` ::: ### Routing-level exceptions Some exceptions (for example, routing 404s) occur before a controller is instantiated, so `rescue_from` won't catch them. Rails handles these via static files in `public/` by default, which is fine for most apps. If you want routing-level errors to render as Inertia pages too, point `exceptions_app` at your router and add error routes. ```ruby # config/application.rb config.exceptions_app = routes ``` ```ruby # config/routes.rb match '/404', to: 'errors#show', defaults: { status: 404 }, via: :all match '/422', to: 'errors#show', defaults: { status: 422 }, via: :all match '/500', to: 'errors#show', defaults: { status: 500 }, via: :all ``` ```ruby # app/controllers/errors_controller.rb class ErrorsController < ApplicationController def show status = params[:status].to_i render inertia: 'ErrorPage', props: { status: }, status: end end ``` Since this goes through the full Rails stack, shared data from `inertia_share` is available on error pages as well. --- --- url: /guide/events.md --- # Events Inertia provides an event system that allows you to "hook into" the various lifecycle events of the library. ## Registering Listeners To register an event listener, use the `router.on()` method. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('start', (event) => { console.log(`Starting a visit to ${event.detail.visit.url}`) }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('start', (event) => { console.log(`Starting a visit to ${event.detail.visit.url}`) }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('start', (event) => { console.log(`Starting a visit to ${event.detail.visit.url}`) }) ``` ::: Under the hood, Inertia uses native browser events, so you can also interact with Inertia events using the typical event methods you may already be familiar with - just be sure to prepend `inertia:` to the event name. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' document.addEventListener('inertia:start', (event) => { console.log(`Starting a visit to ${event.detail.visit.url}`) }) ``` \== React ```jsx import { router } from '@inertiajs/react' document.addEventListener('inertia:start', (event) => { console.log(`Starting a visit to ${event.detail.visit.url}`) }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' document.addEventListener('inertia:start', (event) => { console.log(`Starting a visit to ${event.detail.visit.url}`) }) ``` ::: ## Removing Listeners When you register an event listener, Inertia automatically returns a callback that can be invoked to remove the event listener. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' let removeStartEventListener = router.on('start', (event) => { console.log(`Starting a visit to ${event.detail.visit.url}`) }) // Remove the listener... removeStartEventListener() ``` \== React ```jsx import { router } from '@inertiajs/react' let removeStartEventListener = router.on('start', (event) => { console.log(`Starting a visit to ${event.detail.visit.url}`) }) // Remove the listener... removeStartEventListener() ``` \== Svelte ```js import { router } from '@inertiajs/svelte' let removeStartEventListener = router.on('start', (event) => { console.log(`Starting a visit to ${event.detail.visit.url}`) }) // Remove the listener... removeStartEventListener() ``` ::: Combined with hooks, you can automatically remove the event listener when components unmount. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { useEffect } from 'react' import { router } from '@inertiajs/react' useEffect(() => { return router.on('start', (event) => { console.log(`Starting a visit to ${event.detail.visit.url}`) }) }, []) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' $effect(() => { return router.on('start', (event) => { console.log(`Starting a visit to ${event.detail.visit.url}`) }) }) ``` ::: Alternatively, if you're using native browser events, you can remove the event listener using `removeEventListener()`. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' let startEventListener = (event) => { console.log(`Starting a visit to ${event.detail.visit.url}`) } document.addEventListener('inertia:start', startEventListener) // Remove the listener... document.removeEventListener('inertia:start', startEventListener) ``` \== React ```jsx import { router } from '@inertiajs/react' let startEventListener = (event) => { console.log(`Starting a visit to ${event.detail.visit.url}`) } document.addEventListener('inertia:start', startEventListener) // Remove the listener... document.removeEventListener('inertia:start', startEventListener) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' let startEventListener = (event) => { console.log(`Starting a visit to ${event.detail.visit.url}`) } document.addEventListener('inertia:start', startEventListener) // Remove the listener... document.removeEventListener('inertia:start', startEventListener) ``` ::: ## Cancelling Events Some events, such as `before`, `networkError`, and `httpException`, support cancellation, allowing you to prevent Inertia's default behavior. Just like native events, the event will be cancelled if only one event listener calls `event.preventDefault()`. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('before', (event) => { if (!confirm('Are you sure you want to navigate away?')) { event.preventDefault() } }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('before', (event) => { if (!confirm('Are you sure you want to navigate away?')) { event.preventDefault() } }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('before', (event) => { if (!confirm('Are you sure you want to navigate away?')) { event.preventDefault() } }) ``` ::: For convenience, if you register your event listener using `router.on()`, you can cancel the event by returning `false` from the listener. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('before', (event) => { return confirm('Are you sure you want to navigate away?') }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('before', (event) => { return confirm('Are you sure you want to navigate away?') }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('before', (event) => { return confirm('Are you sure you want to navigate away?') }) ``` ::: Note, browsers do not allow cancelling the native `popstate` event, so preventing forward and back history visits while using Inertia.js is not possible. ## Before The `before` event fires when a request is about to be made to the server. This is useful for intercepting visits. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('before', (event) => { console.log(`About to make a visit to ${event.detail.visit.url}`) }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('before', (event) => { console.log(`About to make a visit to ${event.detail.visit.url}`) }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('before', (event) => { console.log(`About to make a visit to ${event.detail.visit.url}`) }) ``` ::: The primary purpose of this event is to allow you to prevent a visit from happening. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('before', (event) => { return confirm('Are you sure you want to navigate away?') }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('before', (event) => { return confirm('Are you sure you want to navigate away?') }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('before', (event) => { return confirm('Are you sure you want to navigate away?') }) ``` ::: ## Start The `start` event fires when a request to the server has started. This is useful for displaying loading indicators. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('start', (event) => { console.log(`Starting a visit to ${event.detail.visit.url}`) }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('start', (event) => { console.log(`Starting a visit to ${event.detail.visit.url}`) }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('start', (event) => { console.log(`Starting a visit to ${event.detail.visit.url}`) }) ``` ::: The `start` event is not cancelable. ## Progress The `progress` event fires as progress increments during file uploads. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('progress', (event) => { this.form.progress = event.detail.progress.percentage }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('progress', (event) => { this.form.progress = event.detail.progress.percentage }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('progress', (event) => { this.form.progress = event.detail.progress.percentage }) ``` ::: The `progress` event is not cancelable. ## Success The `success` event fires on successful page visits, unless validation errors are present. However, this does *not* include history visits. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('success', (event) => { console.log(`Successfully made a visit to ${event.detail.page.url}`) }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('success', (event) => { console.log(`Successfully made a visit to ${event.detail.page.url}`) }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('success', (event) => { console.log(`Successfully made a visit to ${event.detail.page.url}`) }) ``` ::: The `success` event is not cancelable. ## Flash @available\_since rails=3.17.0 core=2.3.3 The `flash` event fires when [flash data](/guide/flash-data) is received from the server. This is useful for displaying toast notifications or handling temporary data in a central location. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('flash', (event) => { if (event.detail.flash.toast) { showToast(event.detail.flash.toast) } }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('flash', (event) => { if (event.detail.flash.toast) { showToast(event.detail.flash.toast) } }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('flash', (event) => { if (event.detail.flash.toast) { showToast(event.detail.flash.toast) } }) ``` ::: The `flash` event is not cancelable. [Partial reloads](/guide/partial-reloads) will only trigger the event if the flash data has changed. ## Error The `error` event fires when validation errors are present on "successful" page visits. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('error', (event) => { console.log(event.detail.errors) }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('error', (event) => { console.log(event.detail.errors) }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('error', (event) => { console.log(event.detail.errors) }) ``` ::: The `error` event is not cancelable. ## HTTP Exception The `httpException` event (`invalid` in `@inertiajs/core` < 3.0) fires when a non-Inertia response is received from the server, such as an HTML or vanilla JSON response. A valid Inertia response is a response that has the `X-Inertia` header set to `true` with a `json` payload containing [the page object](/guide/the-protocol#the-page-object). This event is fired for all response types, including `200`, `400`, and `500` response codes. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('httpException', (event) => { console.log(`An invalid Inertia response was received.`) console.log(event.detail.response) }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('httpException', (event) => { console.log(`An invalid Inertia response was received.`) console.log(event.detail.response) }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('httpException', (event) => { console.log(`An invalid Inertia response was received.`) console.log(event.detail.response) }) ``` ::: You may cancel the `httpException` event to prevent Inertia from showing the non-Inertia response modal. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('httpException', (event) => { event.preventDefault() // Handle the invalid response yourself... }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('httpException', (event) => { event.preventDefault() // Handle the invalid response yourself... }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('httpException', (event) => { event.preventDefault() // Handle the invalid response yourself... }) ``` ::: ## Network Error The `networkError` event (`error` in `@inertiajs/core` < 3.0) fires on unexpected XHR errors such as network interruptions. In addition, this event fires for errors generated when resolving page components. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('networkError', (event) => { console.log(`An unexpected error occurred during an Inertia visit.`) console.log(event.detail.error) }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('networkError', (event) => { console.log(`An unexpected error occurred during an Inertia visit.`) console.log(event.detail.error) }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('networkError', (event) => { console.log(`An unexpected error occurred during an Inertia visit.`) console.log(event.detail.error) }) ``` ::: You may cancel the `networkError` event to prevent the error from being thrown. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('networkError', (event) => { event.preventDefault() // Handle the error yourself }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('networkError', (event) => { event.preventDefault() // Handle the error yourself }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('networkError', (event) => { event.preventDefault() // Handle the error yourself }) ``` ::: This event will *not* fire for XHR requests that receive `400` and `500` level responses or for non-Inertia responses, as these situations are handled in other ways by Inertia. Please consult the [error handling documentation](/guide/error-handling) for more information. ## Finish The `finish` event fires after an XHR request has completed for both "successful" and "unsuccessful" responses. This event is useful for hiding loading indicators. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('finish', (event) => { NProgress.done() }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('finish', (event) => { NProgress.done() }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('finish', (event) => { NProgress.done() }) ``` ::: The `finish` event is not cancelable. ## Navigate The `navigate` event fires on successful page visits, as well as when navigating through history. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('navigate', (event) => { console.log(`Navigated to ${event.detail.page.url}`) }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('navigate', (event) => { console.log(`Navigated to ${event.detail.page.url}`) }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('navigate', (event) => { console.log(`Navigated to ${event.detail.page.url}`) }) ``` ::: The `navigate` event is not cancelable. ## Prefetching The `prefetching` event fires when the router starts prefetching a page. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('prefetching', (event) => { console.log(`Prefetching ${event.detail.visit.url}`) }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('prefetching', (event) => { console.log(`Prefetching ${event.detail.visit.url}`) }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('prefetching', (event) => { console.log(`Prefetching ${event.detail.visit.url}`) }) ``` ::: The `prefetching` event is not cancelable. ## Prefetched The `prefetched` event fires when the router has successfully prefetched a page. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('prefetched', (event) => { console.log(`Prefetched ${event.detail.visit.url}`) }) ``` \== React ```jsx import { router } from '@inertiajs/react' router.on('prefetched', (event) => { console.log(`Prefetched ${event.detail.visit.url}`) }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('prefetched', (event) => { console.log(`Prefetched ${event.detail.visit.url}`) }) ``` ::: The `prefetched` event is not cancelable. ## Event Callbacks In addition to the global events described throughout this page, Inertia also provides a number of [event callbacks](/guide/manual-visits#event-callbacks) that fire when manually making Inertia visits. --- --- url: /guide/file-uploads.md --- # File Uploads ## `FormData` Conversion When making Inertia requests that include files (even nested files), Inertia will automatically convert the request data into a `FormData` object. This conversion is necessary in order to submit a `multipart/form-data` request via XHR. If you would like the request to always use a `FormData` object regardless of whether a file is present in the data, you may provide the `forceFormData` option when making the request. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.post('/users', data, { forceFormData: true, }) ``` \== React ```js import { router } from '@inertiajs/react' router.post('/users', data, { forceFormData: true, }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.post('/users', data, { forceFormData: true, }) ``` ::: You can learn more about the `FormData` interface via its [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/FormData). ## File Upload Example Let's examine a complete file upload example using Inertia. This example includes both a `name` text input and an `avatar` file input. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { useForm } from '@inertiajs/react' const { data, setData, post, progress } = useForm({ name: null, avatar: null, }) function submit(e) { e.preventDefault() post('/users') } return ( setData('name', e.target.value)} /> setData('avatar', e.target.files[0])} /> {progress && ( {progress.percentage}% )} ) ``` \== Svelte ```svelte
(form.avatar = e.target.files[0])} /> {#if form.progress} {form.progress.percentage}% {/if}
``` ::: This example uses the [Inertia form helper](/guide/forms#form-helper) for convenience, since the form helper provides easy access to the current upload progress. However, you are free to submit your forms using [manual Inertia visits](/guide/manual-visits) as well. ## Multipart Limitations Uploading files using a `multipart/form-data` request is not natively supported in some server-side frameworks when using the `PUT`, `PATCH`, or `DELETE` HTTP methods. The simplest workaround for this limitation is to simply upload files using a `POST` request instead. However, some frameworks, such as [Laravel](https://laravel.com/docs/routing#form-method-spoofing) and [Rails](https://guides.rubyonrails.org/form_helpers.html#forms-with-patch-put-or-delete-methods), support form method spoofing, which allows you to upload the files using `POST`, but have the framework handle the request as a `PUT` or `PATCH` request. This is done by including a `_method` attribute in the data of your request. > \[!NOTE] > For more info see [`Rack::MethodOverride`](https://github.com/rack/rack/blob/main/lib/rack/method_override.rb). :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.post(`/users/${user.id}`, { _method: 'put', avatar: form.avatar, }) ``` \== React ```js import { router } from '@inertiajs/react' router.post(`/users/${user.id}`, { _method: 'put', avatar: form.avatar, }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.post(`/users/${user.id}`, { _method: 'put', avatar: form.avatar, }) ``` ::: --- --- url: /guide/flash-data.md --- # Flash Data @available\_since rails=3.17.0 core=2.3.3 Flash data lets you send one-time data to your frontend that won't reappear when users navigate through browser history. Unlike regular props, flash data isn't persisted in history state, making it ideal for success messages, newly created IDs, or other temporary values. ## Flashing Data Inertia automatically integrates with Rails' standard flash mechanism. Common flash keys like `notice` and `alert` are included in page-level flash data by default. ```ruby class UsersController < ApplicationController def create user = User.new(user_params) if user.save redirect_to users_url, notice: 'User created successfully!' else redirect_to new_user_url, inertia: { errors: user.errors } end end end ``` This works with any Rails flash pattern: ```ruby flash[:notice] = 'Success!' flash[:alert] = 'Something went wrong' redirect_to users_url ``` By default, the following keys are exposed to the frontend: `notice`, `alert`. ## Custom Flash Data For custom data beyond the default allowlisted keys, use `flash.inertia`: ```ruby class UsersController < ApplicationController def create user = User.new(user_params) if user.save flash.inertia[:new_user_id] = user.id flash.inertia[:toast] = { message: 'Created!', type: 'success' } redirect_to users_url else redirect_to new_user_url, inertia: { errors: user.errors } end end end ``` This follows standard Rails flash patterns - `flash.inertia` is a scoped namespace within flash. You can also pass flash data inline with `redirect_to`: ```ruby redirect_to users_url, flash: { inertia: { new_user_id: user.id } } ``` For current-request-only flash (not persisted across redirects), use `.now`: ```ruby flash.now.inertia[:temporary] = 'This request only' render inertia: 'MyComponent' ``` Flash data is scoped to the current request. Rails automatically persists it when redirecting. After the flash data is sent to the client, it is cleared and will not appear in subsequent requests. ## Configuring Allowed Flash Keys You may customize which Rails flash keys are exposed to the frontend: ```ruby # config/initializers/inertia_rails.rb InertiaRails.configure do |config| # Default: Only safe keys config.flash_keys = %i[notice alert] end ``` ## Accessing Flash Data Flash data is available on `page.flash`. You may also listen for the global `flash` event or use the `onFlash` callback. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { usePage } from '@inertiajs/react' export default function Layout({ children }) { const { flash } = usePage() return ( <> {flash.toast &&
{flash.toast.message}
} {children} ) } ``` \== Svelte ```svelte {#if page.flash.toast}
{page.flash.toast.message}
{/if} ``` ::: ## The `onFlash` Callback You may use the `onFlash` callback to handle flash data when making requests. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.post('/users', data, { onFlash: ({ newUserId }) => { form.userId = newUserId }, }) ``` \== React ```js import { router } from '@inertiajs/react' router.post('/users', data, { onFlash: ({ newUserId }) => { form.userId = newUserId }, }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.post('/users', data, { onFlash: ({ newUserId }) => { form.userId = newUserId }, }) ``` ::: ## Global Flash Event You may use the global `flash` event to handle flash data in a central location, such as a layout component. For more information on events, see the [events documentation](/guide/events). :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('flash', (event) => { if (event.detail.flash.toast) { showToast(event.detail.flash.toast) } }) ``` \== React ```js import { router } from '@inertiajs/react' router.on('flash', (event) => { if (event.detail.flash.toast) { showToast(event.detail.flash.toast) } }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('flash', (event) => { if (event.detail.flash.toast) { showToast(event.detail.flash.toast) } }) ``` ::: > \[!WARNING] > Event listeners registered inside components should be cleaned up when the > component unmounts to prevent them from accumulating and firing multiple > times. This is especially important in non-persistent layouts. See [removing > event listeners](/guide/events#removing-listeners) for more information. Native browser events are also supported. :::tabs key:frameworks \== Vue ```js document.addEventListener('inertia:flash', (event) => { console.log(event.detail.flash) }) ``` \== React ```js document.addEventListener('inertia:flash', (event) => { console.log(event.detail.flash) }) ``` \== Svelte ```js document.addEventListener('inertia:flash', (event) => { console.log(event.detail.flash) }) ``` ::: The `flash` event is not cancelable and fires on every response that carries flash data. ## Client-Side Flash You may set flash data on the client without a server request using the `router.flash()` method. Values are merged with existing flash data. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.flash('foo', 'bar') router.flash({ foo: 'bar' }) ``` \== React ```js import { router } from '@inertiajs/react' router.flash('foo', 'bar') router.flash({ foo: 'bar' }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.flash('foo', 'bar') router.flash({ foo: 'bar' }) ``` ::: A callback may also be passed to access the current flash data or replace it entirely. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.flash((current) => ({ ...current, bar: 'baz' })) router.flash(() => ({})) ``` \== React ```js import { router } from '@inertiajs/react' router.flash((current) => ({ ...current, bar: 'baz' })) router.flash(() => ({})) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.flash((current) => ({ ...current, bar: 'baz' })) router.flash(() => ({})) ``` ::: ## TypeScript You may configure the flash data type globally using [TypeScript's declaration merging](/guide/typescript#flash-data). ## Testing For information on testing flash data, see the [testing documentation](/guide/testing#testing-flash-data). --- --- url: /guide/forms.md --- # Forms Inertia provides two primary ways to build forms: the `
` component and the `useForm` helper. Both integrate with your server-side framework's validation and handle form submissions without full page reloads. ## Form Component @available\_since core=2.1.0 Inertia provides a `` component that behaves much like a classic HTML form, but uses Inertia under the hood to avoid full page reloads. This is the simplest way to get started with forms in Inertia. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Form } from '@inertiajs/react' export default () => (
) ``` \== Svelte ```svelte
``` ::: Just like a traditional HTML form, there is no need to attach `v-model`an `onChange` handler`bind:` to your input fields, just give each input a `name` attribute and a `defaultValue` (if applicable) and the `Form` component will handle the data submission for you. The component also supports nested data structures, file uploads, and dotted key notation. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx
``` \== Svelte ```svelte
``` ::: You can pass a `transform` prop to modify the form data before submission. This is useful for injecting additional fields or transforming existing data, although hidden inputs work too. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx
({ ...data, user_id: 123 })} >
``` \== Svelte ```svelte
({ ...data, user_id: 123 })} >
``` ::: ### Default Values You can set default values for form inputs using standard HTML attributes. Use `defaultValue``defaultValue``defaultValue` (`value` for Svelte < `5.6.0`) for text inputs and textareas, and `defaultChecked``defaultChecked``defaultChecked` (`checked` for Svelte < `5.6.0`) for checkboxes and radios. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx
``` \== Svelte ```svelte
``` ::: ### Checkbox Inputs When working with checkboxes, you may want to add an explicit `value` attribute such as `value="1"`. Without a value attribute, checked checkboxes will submit as `"on"`, which some server-side validation rules may not recognize as a proper boolean value. ### Slot Props The `
` component exposes reactive state and helper methods through its default slot, giving you access to form processing state, errors, and utility functions. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx {({ errors, hasErrors, processing, progress, wasSuccessful, recentlySuccessful, setError, clearErrors, resetAndClearErrors, defaults, isDirty, reset, submit, }) => ( <> {errors.name &&
{errors.name}
} {wasSuccessful &&
User created successfully!
} )}
``` \== Svelte ```svelte
{#snippet children({ errors, hasErrors, processing, progress, wasSuccessful, recentlySuccessful, setError, clearErrors, resetAndClearErrors, defaults, isDirty, reset, submit, })} {#if errors.name}
{errors.name}
{/if} {#if wasSuccessful}
User created successfully!
{/if} {/snippet}
``` ::: #### `defaults` method @available\_since core=2.1.1 The `defaults` method allows you to update the form's default values to match the current field values. When called, subsequent `reset()` calls will restore fields to these new defaults, and the `isDirty` property will track changes from these updated defaults. Unlike `useForm`, this method accepts no arguments and always uses all current form values. #### `errors` object The `errors` object uses dotted notation for nested fields, allowing you to display validation messages for complex form structures. :::tabs key:frameworks \== Vue ```vue
{{ errors['user.name'] }}
``` \== React ```jsx
{({ errors }) => ( <> {errors['user.name'] &&
{errors['user.name']}
} )}
``` \== Svelte ```svelte
{#snippet children({ errors })} {#if errors['user.name']}
{errors['user.name']}
{/if} {/snippet}
``` ::: ### Props and Options In addition to `action` and `method`, the `
` component accepts several props. Many of them are identical to the options available in Inertia's [visit options](/guide/manual-visits). :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx ({ ...data, timestamp: Date.now() })} optimistic={(props, data) => ({ ...props })} invalidateCacheTags={['users', 'dashboard']} disableWhileProcessing options={{ preserveScroll: true, preserveState: true, preserveUrl: true, replace: true, only: ['users', 'flash'], except: ['secret'], reset: ['page'], }} >
``` \== Svelte ```svelte
({ ...data, timestamp: Date.now() })} optimistic={(props, data) => ({ ...props })} invalidateCacheTags={['users', 'dashboard']} disableWhileProcessing options={{ preserveScroll: true, preserveState: true, preserveUrl: true, replace: true, only: ['users', 'flash'], except: ['secret'], reset: ['page'], }} >
``` ::: Some props are intentionally grouped under `options` instead of being top-level to avoid confusion. For example, `only`, `except`, and `reset` relate to *partial reloads*, not *partial submissions*. The general rule: top-level props are for the form submission itself, while `options` control how Inertia handles the subsequent visit. When setting the `disableWhileProcessing``disableWhileProcessing``disable-while-processing` prop, the `Form` component will add the `inert` attribute to the HTML `form` tag while the form is processing to prevent user interaction. To style the form while it's processing, you can target the inert form in the following ways. :::tabs key:css \== Tailwind 4 ```jsx
{/* Your form fields here */}
``` \== CSS ```css form[inert] { opacity: 0.5; pointer-events: none; } ``` ::: ### Events The `
` component emits all the standard visit [events](/guide/events) for form submissions. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx
``` \== Svelte ```svelte
``` ::: ### Resetting the Form @available\_since core=2.1.2 The `Form` component provides several attributes that allow you to reset the form after a submission. `resetOnSuccess` may be used to reset the form after a successful submission. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx // Reset the entire form on success
// Reset specific fields on success
``` \== Svelte ```svelte
``` ::: `resetOnError` may be used to reset the form after errors. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx // Reset the entire form on success
// Reset specific fields on success
``` \== Svelte ```svelte
``` ::: ### Setting New Default Values @available\_since core=2.1.2 The `Form` component provides the `setDefaultsOnSuccess` attribute to set the current form values as the new defaults after a successful submission. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx
``` \== Svelte ```svelte
``` ::: ### Dotted Key Notation The `
` component supports dotted key notation for creating nested objects from flat input names. This provides a convenient way to structure form data. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx
``` \== Svelte ```svelte
``` ::: The example above would generate the following data structure. ```json { "user": { "name": "John Doe", "skills": ["JavaScript"] }, "address": { "street": "123 Main St" } } ``` If you need literal dots in your field names (not as nested object separators), you can escape them using backslashes. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx
``` \== Svelte ```svelte
``` ::: The example above would generate the following data structure. ```json { "app.name": "My Application", "settings": { "theme.mode": "dark" } } ``` ### Programmatic Access You can access the form's methods programmatically using refs. This provides an alternative to [slot props](#slot-props) when you need to trigger form actions from outside the form. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { useRef } from 'react' import { Form } from '@inertiajs/react' export default function CreateUser() { const formRef = useRef() const handleSubmit = () => { formRef.current.submit() } return ( <>
) } ``` \== Svelte ```svelte
``` ::: In React and Vue, refs provide access to all form methods and reactive state. In Svelte, refs expose only methods, so reactive state like `isDirty` and `errors` should be accessed via [slot props](#slot-props) instead. ### Form Context @available\_since core=2.3.9 Deeply nested child components may need access to form state or methods without passing props through multiple levels. The `useFormContext` hook provides access to the parent `
` component's state and methods from any child component. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { useFormContext } from '@inertiajs/react' export default function FormActions() { const form = useFormContext() if (!form) { return null } return (
{form.isDirty && Unsaved changes} {form.errors.name && {form.errors.name}}
) } ``` \== Svelte ```svelte {#if form} {#if form.isDirty}Unsaved changes{/if} {#if form.errors.name}{form.errors.name}{/if} {/if} ``` ::: The context provides access to all the same properties and methods available through [slot props](#slot-props). > \[!NOTE] > Both the `` component and `useFormContext` accept generic type parameters for type-safe errors and slot props. See the [TypeScript](/guide/typescript#form-component) documentation for details. ### Precognition @available\_since core=2.3.0 The `` component includes built-in support for Precognition, enabling real-time form validation without duplicating your server-side validation rules on the client. > \[!NOTE] > Precognition requires server-side support. See the [precognition section](/guide/validation.md#precognition) of the validation documentation for Rails setup instructions. Once your server is configured, call `validate()` with a field name to trigger validation for that field. The `invalid()` helper checks if a field has validation errors, while `validating` indicates when a request is in progress. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx {({ errors, invalid, validate, validating }) => ( <> validate('name')} /> {invalid('name') &&

{errors.name}

} validate('email')} /> {invalid('email') &&

{errors.email}

} {validating &&

Validating...

} )}
``` \== Svelte ```svelte
{#snippet children({ errors, invalid, validate, validating })} validate('name')} /> {#if invalid('name')}

{errors.name}

{/if} validate('email')} /> {#if invalid('email')}

{errors.email}

{/if} {#if validating}

Validating...

{/if} {/snippet}
``` ::: You may also use the `valid()` helper to check if a field has passed validation. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx
{({ errors, invalid, valid, validate }) => ( <> validate('email')} /> {valid('email') &&

Valid email address

} {invalid('email') &&

{errors.email}

} )}
``` \== Svelte ```svelte
{#snippet children({ errors, invalid, valid, validate })} validate('email')} /> {#if valid('email')}

Valid email address

{/if} {#if invalid('email')}

{errors.email}

{/if} {/snippet}
``` ::: > \[!WARNING] > A form input will only appear as valid or invalid once it has changed and a > validation response has been received. Calling `validate('field')` will not send a validation request until the field's value differs from the initial data. #### Validating Multiple Fields You may validate multiple fields at once using the `only` option. This is particularly useful when building wizard-style forms where you want to validate all visible fields before proceeding to the next step. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx
{({ validate }) => ( <> {/* Step 1 fields */} )}
``` \== Svelte ```svelte
{#snippet children({ validate })} {/snippet}
``` ::: #### Touch and Validate The `touch()` method marks fields as "touched" without triggering validation. You may then validate all touched fields by calling `validate()` without arguments. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx
{({ validate, touch, touched }) => ( <> touch('name')} /> touch('email')} /> touch('phone')} /> {touched('name') &&

Name has been touched

} )}
``` \== Svelte ```svelte
{#snippet children({ validate, touch, touched })} touch('name')} /> touch('email')} /> touch('phone')} /> {#if touched('name')}

Name has been touched

{/if} {/snippet}
``` ::: The `touched()` helper may also be called without arguments to check if any field has been touched. The `reset()` method clears the touched state for reset fields. #### Options The `validate()` method accepts an options object with callbacks and configuration. ```js validate('username', { onSuccess: () => { // Validation passed... }, onValidationError: (response) => { // Validation failed (422 response)... }, onBeforeValidation: (newRequest, oldRequest) => { // Return false to prevent validation... }, onFinish: () => { // Always runs after validation... }, }) ``` You may also call `validate()` with only an options object to validate specific fields. ```js validate({ only: ['name', 'email'], onSuccess: () => goToNextStep(), }) ``` Validation requests are automatically debounced. The first request fires immediately, then subsequent changes are debounced (1500ms by default). You may customize this timeout. :::tabs key:frameworks \== Vue ```vue
``` \== React ```jsx
{/* ... */}
``` \== Svelte ```svelte
``` ::: By default, files are excluded from validation requests to avoid unnecessary uploads. You may enable file validation when you need to validate file inputs like size or mime type. :::tabs key:frameworks \== Vue ```vue
``` \== React ```jsx
{/* ... */}
``` \== Svelte ```svelte
``` ::: By default, validation errors are simplified to strings (the first error message). You may keep errors as arrays to display all error messages for fields with multiple validation rules. :::tabs key:frameworks \== Vue ```vue
``` \== React ```jsx
{/* ... */}
``` \== Svelte ```svelte
``` ::: ## Form Helper In addition to the `
` component, Inertia also provides a `useForm` helper for when you need programmatic control over your form's data and submission behavior. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { useForm } from '@inertiajs/react' const { data, setData, post, processing, errors } = useForm({ email: '', password: '', remember: false, }) function submit(e) { e.preventDefault() post('/login') } return ( setData('email', e.target.value)} /> {errors.email &&
{errors.email}
} setData('password', e.target.value)} /> {errors.password &&
{errors.password}
} setData('remember', e.target.checked)} /> Remember Me
) ``` \== Svelte ```svelte
{#if form.errors.email}
{form.errors.email}
{/if} {#if form.errors.password}
{form.errors.password}
{/if} Remember Me
``` ::: To submit the form, you may use the `get`, `post`, `put`, `patch` and `delete` methods. :::tabs key:frameworks \== Vue ```js form.submit(method, url, options) form.get(url, options) form.post(url, options) form.put(url, options) form.patch(url, options) form.delete(url, options) ``` \== React ```js const { submit, get, post, put, patch, delete: destroy, } = useForm({ /*...*/ }) submit(method, url, options) get(url, options) post(url, options) put(url, options) patch(url, options) destroy(url, options) ``` \== Svelte ```js form.submit(method, url, options) form.get(url, options) form.post(url, options) form.put(url, options) form.patch(url, options) form.delete(url, options) ``` ::: The submit methods support all of the typical [visit options](/guide/manual-visits), such as `preserveState`, `preserveScroll`, and event callbacks, which can be helpful for performing tasks on successful form submissions. For example, you might use the `onSuccess` callback to reset inputs to their original state. :::tabs key:frameworks \== Vue ```js form.post('/profile', { preserveScroll: true, onSuccess: () => form.reset('password'), }) ``` \== React ```js const { post, reset } = useForm({ /*...*/ }) post('/profile', { preserveScroll: true, onSuccess: () => reset('password'), }) ``` \== Svelte ```js form.post('/profile', { preserveScroll: true, onSuccess: () => form.reset('password'), }) ``` ::: If you need to modify the form data before it's sent to the server, you can do so via the `transform()` method. :::tabs key:frameworks \== Vue ```js form .transform((data) => ({ ...data, remember: data.remember ? 'on' : '', })) .post('/login') ``` \== React ```js const { transform } = useForm({ /*...*/ }) transform((data) => ({ ...data, remember: data.remember ? 'on' : '', })) ``` \== Svelte ```js form .transform((data) => ({ ...data, remember: data.remember ? 'on' : '', })) .post('/login') ``` ::: You can use the `processing` property to track if a form is currently being submitted. This can be helpful for preventing double form submissions by disabling the submit button. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx const { processing } = useForm({ /*...*/ }) ``` \== Svelte ```svelte ``` ::: If your form is uploading files, the current progress event is available via the `progress` property, allowing you to easily display the upload progress. :::tabs key:frameworks \== Vue ```vue {{ form.progress.percentage }}% ``` \== React ```jsx const { progress } = useForm({ /*...*/ }) { progress && ( {progress.percentage}% ) } ``` \== Svelte ```svelte {#if form.progress} {form.progress.percentage}% {/if} ``` ::: ### Form Errors If there are form validation errors, they are available via the `errors` property. When building Laravel powered Inertia applications, form errors will automatically be populated when your application throws instances of `ValidationException`, such as when using `$request->validate()`. :::tabs key:frameworks \== Vue ```vue
{{ form.errors.email }}
``` \== React ```jsx const { errors } = useForm({ /*...*/ }) { errors.email &&
{errors.email}
} ``` \== Svelte ```svelte {#if form.errors.email}
{form.errors.email}
{/if} ``` ::: For a more thorough discussion of form validation and errors, please consult the [validation documentation](/guide/validation). To determine if a form has any errors, you may use the `hasErrors` property. To clear form errors, use the `clearErrors()` method. :::tabs key:frameworks \== Vue ```js // Clear all errors... form.clearErrors() // Clear errors for specific fields... form.clearErrors('field', 'anotherfield') ``` \== React ```js const { clearErrors } = useForm({ /*...*/ }) // Clear all errors... clearErrors() // Clear errors for specific fields... clearErrors('field', 'anotherfield') ``` \== Svelte ```js // Clear all errors... form.clearErrors() // Clear errors for specific fields... form.clearErrors('field', 'anotherfield') ``` ::: If you're using client-side input validation libraries or do client-side validation manually, you can set your own errors on the form using the `setError()` method. :::tabs key:frameworks \== Vue ```js // Set a single error... form.setError('field', 'Your error message.') // Set multiple errors at once... form.setError({ foo: 'Your error message for the foo field.', bar: 'Some other error for the bar field.', }) ``` \== React ```js const { setError } = useForm({ /*...*/ }) // Set a single error... setError('field', 'Your error message.') // Set multiple errors at once... setError({ foo: 'Your error message for the foo field.', bar: 'Some other error for the bar field.', }) ``` \== Svelte ```js // Set a single error form.setError('field', 'Your error message.') // Set multiple errors at once form.setError({ foo: 'Your error message for the foo field.', bar: 'Some other error for the bar field.', }) ``` ::: Unlike an actual form submission, the page's props remain unchanged when manually setting errors on a form instance. When a form has been successfully submitted, the `wasSuccessful` property will be `true`. In addition to this, forms have a `recentlySuccessful` property, which will be set to `true` for two seconds after a successful form submission. This property can be utilized to show temporary success messages. You may customize the duration of the `recentlySuccessful` state by setting the `form.recentlySuccessfulDuration` option in your [application defaults](/guide/client-side-setup#configuring-defaults). The default value is `2000` milliseconds. ### Resetting the Form To reset the form's values back to their default values, you can use the `reset()` method. :::tabs key:frameworks \== Vue ```js // Reset the form... form.reset() // Reset specific fields... form.reset('field', 'anotherfield') ``` \== React ```js const { reset } = useForm({ /*...*/ }) // Reset the form... reset() // Reset specific fields... reset('field', 'anotherfield') ``` \== Svelte ```js // Reset the form... form.reset() // Reset specific fields... form.reset('field', 'anotherfield') ``` ::: @available\_since core=2.0.15 Sometimes, you may want to restore your form fields to their default values and clear any validation errors at the same time. Instead of calling `reset()` and `clearErrors()` separately, you can use the `resetAndClearErrors()` method, which combines both actions into a single call. :::tabs key:frameworks \== Vue ```js // Reset the form and clear all errors... form.resetAndClearErrors() // Reset specific fields and clear their errors... form.resetAndClearErrors('field', 'anotherfield') ``` \== React ```js const { resetAndClearErrors } = useForm({ /*...*/ }) // Reset the form and clear all errors... resetAndClearErrors() // Reset specific fields and clear their errors... resetAndClearErrors('field', 'anotherfield') ``` \== Svelte ```js // Reset the form and clear all errors... form.resetAndClearErrors() // Reset specific fields and clear their errors... form.resetAndClearErrors('field', 'anotherfield') ``` ::: ### Setting New Default Values If your form's default values become outdated, you can use the `defaults()``setDefaults()``defaults()` method to update them. Then, the form will be reset to the correct values the next time the `reset()` method is invoked. :::tabs key:frameworks \== Vue ```js // Set the form's current values as the new defaults... form.defaults() // Update the default value of a single field... form.defaults('email', 'updated-default@example.com') // Update the default value of multiple fields... form.defaults({ name: 'Updated Example', email: 'updated-default@example.com', }) ``` \== React ```js const { setDefaults } = useForm({ /*...*/ }) // Set the form's current values as the new defaults... setDefaults() // Update the default value of a single field... setDefaults('email', 'updated-default@example.com') // Update the default value of multiple fields... setDefaults({ name: 'Updated Example', email: 'updated-default@example.com', }) ``` \== Svelte ```js // Set the form's current values as the new defaults... form.defaults() // Update the default value of a single field... form.defaults('email', 'updated-default@example.com') // Change the default value of multiple fields... form.defaults({ name: 'Updated Example', email: 'updated-default@example.com', }) ``` ::: ### Form Field Change Tracking To determine if a form has any changes, you may use the `isDirty` property. :::tabs key:frameworks \== Vue ```vue
There are unsaved form changes.
``` \== React ```jsx const { isDirty } = useForm({ /*...*/ }) { isDirty &&
There are unsaved form changes.
} ``` \== Svelte ```svelte {#if form.isDirty}
There are unsaved form changes.
{/if} ``` ::: ### Canceling Form Submissions To cancel a form submission, use the `cancel()` method. :::tabs key:frameworks \== Vue ```js form.cancel() ``` \== React ```js const { cancel } = useForm({ /*...*/ }) cancel() ``` \== Svelte ```js form.cancel() ``` ::: ### Form Data and History State To instruct Inertia to store a form's data and errors in [history state](/guide/remembering-state), you can provide a unique form key as the first argument when instantiating your form. :::tabs key:frameworks \== Vue ```js import { useForm } from '@inertiajs/vue3' const form = useForm('CreateUser', data) const form = useForm(`EditUser:${user.id}`, data) ``` \== React ```js import { useForm } from '@inertiajs/react' const form = useForm('CreateUser', data) const form = useForm(`EditUser:${user.id}`, data) ``` \== Svelte ```js import { useForm } from '@inertiajs/svelte' const form = useForm('CreateUser', data) const form = useForm(`EditUser:${user.id}`, data) ``` ::: #### Excluding Fields @available\_since core=2.3.7 Sensitive fields like passwords may be excluded from history state using the `dontRemember()` method. :::tabs key:frameworks \== Vue ```js import { useForm } from '@inertiajs/vue3' const form = useForm('LoginForm', { email: '', password: '', }).dontRemember('password') ``` \== React ```js import { useForm } from '@inertiajs/react' const form = useForm('LoginForm', { email: '', password: '', }).dontRemember('password') ``` \== Svelte ```js import { useForm } from '@inertiajs/svelte' const form = useForm('LoginForm', { email: '', password: '', }).dontRemember('password') ``` ::: Multiple fields may be excluded by passing additional arguments. ```js form.dontRemember('password', 'password_confirmation') ``` > \[!NOTE] > Some browsers trigger a "save password" prompt whenever password field values > are written to history state, even without form submission. Excluding password > fields avoids this issue. ### Precognition @available\_since core=2.3.0 Just like the `
` component, the `useForm` helper supports [Precognition](#precognition) for real-time validation. You may enable it by chaining the `withPrecognition()` method with the HTTP method and endpoint for validation requests. > \[!NOTE] > Precognition requires server-side support. See the [precognition section](/guide/validation.md#precognition) of the validation documentation for Rails setup instructions. :::tabs key:frameworks \== Vue ```js import { useForm } from '@inertiajs/vue3' const form = useForm({ name: '', email: '', }).withPrecognition('post', '/users') ``` \== React ```js import { useForm } from '@inertiajs/react' const form = useForm({ name: '', email: '', }).withPrecognition('post', '/users') ``` \== Svelte ```js import { useForm } from '@inertiajs/svelte' const form = useForm({ name: '', email: '', }).withPrecognition('post', '/users') ``` ::: For backwards compatibility with the `laravel-precognition` packages, you may also pass the method and URL as the first arguments to `useForm()`. ```js const form = useForm('post', '/users', { name: '', email: '', }) ``` Once Precognition is enabled, call `validate()` with a field name to trigger validation for that field. The `invalid()` helper checks if a field has validation errors, while `validating` indicates when a request is in progress. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { useForm } from '@inertiajs/react' const { data, setData, post, errors, validating, validate, invalid } = useForm( 'post', '/users', { name: '', email: '', }, ) function submit(e) { e.preventDefault() post('/users') } return ( setData('name', e.target.value)} onBlur={() => validate('name')} /> {invalid('name') &&

{errors.name}

} setData('email', e.target.value)} onBlur={() => validate('email')} /> {invalid('email') &&

{errors.email}

} {validating &&

Validating...

}
) ``` \== Svelte ```svelte
{ e.preventDefault() form.post('/users') }} > form.validate('name')} /> {#if form.invalid('name')}

{form.errors.name}

{/if} form.validate('email')} /> {#if form.invalid('email')}

{form.errors.email}

{/if} {#if form.validating}

Validating...

{/if}
``` ::: You may also use the `valid()` helper to check if a field has passed validation. #### Touch and Validate The `touch()` method marks fields as "touched" without triggering validation. You may then validate all touched fields by calling `validate()` without arguments. The `touched()` helper checks if a field has been touched. The `reset()` method clears the touched state for reset fields. :::tabs key:frameworks \== Vue ```vue

Name has been touched

``` \== React ```jsx setData('name', e.target.value)} onBlur={() => touch('name')} /> setData('email', e.target.value)} onBlur={() => touch('email')} /> {touched('name') &&

Name has been touched

} ``` \== Svelte ```svelte form.touch('name')} /> form.touch('email')} /> {#if form.touched('name')}

Name has been touched

{/if} ``` ::: #### Options Validation requests are automatically debounced. The first request fires immediately, then subsequent changes are debounced (1500ms by default). You may customize this timeout using `setValidationTimeout()`. :::tabs key:frameworks \== Vue ```js const form = useForm('post', '/users', { name: '', }).setValidationTimeout(500) ``` \== React ```js const form = useForm('post', '/users', { name: '', }) form.setValidationTimeout(500) ``` \== Svelte ```js const form = useForm('post', '/users', { name: '', }) form.setValidationTimeout(500) ``` ::: By default, files are excluded from validation requests to avoid unnecessary uploads. You may enable file validation using `validateFiles()`. :::tabs key:frameworks \== Vue ```js const form = useForm('post', '/users', { avatar: null, }).validateFiles() ``` \== React ```js const form = useForm('post', '/users', { avatar: null, }) form.validateFiles() ``` \== Svelte ```js const form = useForm('post', '/users', { avatar: null, }) form.validateFiles() ``` ::: By default, validation errors are simplified to strings (the first error message). You can indicate you would like all errors as arrays using `withAllErrors()`. :::tabs key:frameworks \== Vue ```js const form = useForm('post', '/users', { name: '', }).withAllErrors() ``` \== React ```js const form = useForm('post', '/users', { name: '', }) form.withAllErrors() ``` \== Svelte ```js const form = useForm('post', '/users', { name: '', }) form.withAllErrors() ``` ::: With Precognition enabled, you may call `submit()` without arguments to submit to the configured endpoint. ## Server-Side Responses When using Inertia, you don't typically inspect form responses client-side like you would with traditional XHR/fetch requests. Instead, your server-side route or controller issues a [redirect](/guide/redirects) response after processing the form, often redirecting to a success page. ```ruby class UsersController < ApplicationController def create user = User.new(user_params) if user.save redirect_to users_url else redirect_to new_user_url, inertia: { errors: user.errors } end end private def user_params params.require(:user).permit(:name, :email) end end ``` This redirect-based approach works with all form submission methods: the `
` component, `useForm` helper, and manual router submissions. It makes handling Inertia forms feel very similar to classic server-side form submissions. ## Server-Side Validation Both the `` component and `useForm` helper automatically handle server-side validation errors. When your server returns validation errors, they're automatically available in the `errors` object without any additional configuration. Unlike traditional XHR/fetch requests where you might check for a `422` status code, Inertia handles validation errors as part of its redirect-based flow, just like classic server-side form submissions, but without the full page reload. For a complete guide on validation error handling, including error bags and advanced scenarios, see the [validation documentation](/guide/validation). ## Manual Form Submissions It's also possible to submit forms manually using Inertia's `router` methods directly, without using the `` component or `useForm` helper: :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { useState } from 'react' import { router } from '@inertiajs/react' export default function Edit() { const [values, setValues] = useState({ first_name: '', last_name: '', email: '', }) function handleChange(e) { const key = e.target.id const value = e.target.value setValues((values) => ({ ...values, [key]: value, })) } function handleSubmit(e) { e.preventDefault() router.post('/users', values) } return (
) } ``` \== Svelte ```svelte
``` ::: ## File Uploads When making requests or form submissions that include files, Inertia will automatically convert the request data into a `FormData` object. This works with the `
` component, `useForm` helper, and manual router submissions. For more information on file uploads, including progress tracking, see the [file uploads documentation](/guide/file-uploads). ## Optimistic Updates Both the `` component and `useForm` helper support optimistic updates, allowing you to update the UI immediately before the server responds. For more details, see the [optimistic updates](/guide/optimistic-updates) documentation. ## Non-Inertia Submissions Using Inertia to submit forms works great for the vast majority of situations. For standalone HTTP requests that don't trigger page visits, you may use the [`useHttp`](/guide/http-requests) hook, which provides the same developer experience as `useForm`. You're also free to make plain XHR or `fetch` requests using the library of your choice. --- --- url: /cookbook/handling-validation-error-types.md --- # Handling Rails validation error types When using Inertia Rails with TypeScript, you might encounter a mismatch between the way Rails and Inertia handle validation errors. * Inertia's `useForm` hook expects the `errors` object to have values as single strings (e.g., `"This field is required"`). * Rails model errors (`model.errors`), however, provide an array of strings for each field (e.g., `["This field is required", "Must be unique"]`). If you pass `inertia: { errors: user.errors }` directly from a Rails controller, this mismatch will cause a type conflict. We'll explore two options to resolve this issue. ## Option 1: Adjust Inertia types You can update the TypeScript definitions to match the Rails error format (arrays of strings). Create a custom type definition file in your project: :::tabs key:frameworks \== Vue ```typescript // frontend/app/types/inertia-rails.d.ts import type { FormDataConvertible, FormDataKeys } from '@inertiajs/core' import type { InertiaFormProps as OriginalProps } from '@inertiajs/vue3' type FormDataType = Record declare module '@inertiajs/vue3' { interface InertiaFormProps extends Omit< OriginalProps, 'errors' | 'setError' > { errors: Partial, string[]>> setError(field: FormDataKeys, value: string[]): this setError(errors: Record, string[]>): this } export type InertiaForm = TForm & InertiaFormProps export { InertiaFormProps, InertiaForm } export function useForm( data: TForm | (() => TForm), ): InertiaForm export function useForm( rememberKey: string, data: TForm | (() => TForm), ): InertiaForm } ``` \== React ```typescript // frontend/app/types/inertia-rails.d.ts import type { FormDataConvertible, FormDataKeys } from '@inertiajs/core' import type { InertiaFormProps as OriginalProps } from '@inertiajs/react' type FormDataType = Record declare module '@inertiajs/react' { interface InertiaFormProps extends Omit< OriginalProps, 'errors' | 'setError' > { errors: Partial, string[]>> setError(field: FormDataKeys, value: string[]): void setError(errors: Record, string[]>): void } export { InertiaFormProps } export function useForm( initialValues?: TForm, ): InertiaFormProps export function useForm( rememberKey: string, initialValues?: TForm, ): InertiaFormProps } ``` \== Svelte ```typescript // frontend/app/types/inertia-rails.d.ts import type { FormDataConvertible, FormDataKeys } from '@inertiajs/core' import type { InertiaFormProps as OriginalProps } from '@inertiajs/svelte' import type { Writable } from 'svelte/store' type FormDataType = Record declare module '@inertiajs/svelte' { interface InertiaFormProps extends Omit< OriginalProps, 'errors' | 'setError' > { errors: Partial, string[]>> setError(field: FormDataKeys, value: string[]): this setError(errors: Record, string[]>): this } type InertiaForm = InertiaFormProps & TForm export { InertiaFormProps, InertiaForm } export function useForm( data: TForm | (() => TForm), ): Writable> export function useForm( rememberKey: string, data: TForm | (() => TForm), ): Writable> } ``` ::: This tells TypeScript to expect errors as arrays of strings, matching Rails' format. > \[!NOTE] > Make sure that `d.ts` files are referenced in your `tsconfig.json` or `tsconfig.app.json`. If it reads something like `"include": ["app/frontend/**/*.ts"]` or `"include": ["app/frontend/**/*"]` and your `d.ts` file is inside `app/frontend`, it should work. ## Option 2: Serialize errors in Rails You can add a helper on the Rails backend to convert error arrays into single strings before sending them to Inertia. 1. Add a helper method (e.g., in `ApplicationController`): ```ruby def inertia_errors(model) { errors: model.errors.to_hash(true).transform_values(&:to_sentence) } end ``` This combines multiple error messages for each field into a single string. 2. Use the helper when redirecting with errors: ```ruby redirect_back inertia: inertia_errors(model) ``` This ensures the errors sent to the frontend are single strings, matching Inertia's default expectations. --- --- url: /guide/history-encryption.md --- # History Encryption Imagine a scenario where your user is authenticated, browses privileged information on your site, then logs out. If they press the back button, they can still see the privileged information that is stored in the window's history state. This is a security risk. To prevent this, Inertia.js provides a history encryption feature. ## How It Works When you instruct Inertia to encrypt your app's history, it uses the browser's built-in [`crypto` api](https://developer.mozilla.org/en-US/docs/Web/API/Crypto) to encrypt the current page's data before pushing it to the history state. We store the corresponding key in the browser's session storage. When the user navigates back to a page, we decrypt the data using the key stored in the session storage. Once you instruct Inertia to clear your history state, we simply clear the existing key from session storage roll a new one. If we attempt to decrypt the history state with the new key, it will fail and Inertia will make a fresh request back to your server for the page data. History encryption relies on `window.crypto.subtle` which is only available in secure environments (sites with SSL enabled). ## Opting in History encryption is an opt-in feature. There are several methods for enabling it: ### Global Encryption If you'd like to enable history encryption globally, set the `history_encrypt` config value to `true`. You are able to opt out of encryption on specific pages by passing `false` to the `encrypt_history` option. ```ruby render inertia: {}, encrypt_history: false ``` ### Per-request Encryption To encrypt the history of an individual request, simply pass `true` to the `encrypt_history` option. ```ruby render inertia: {}, encrypt_history: true ``` ### Encrypt Middleware You can also enable history encryption for all actions in a controller by setting the `encrypt_history` config value in the controller. ```ruby class DashboardController < ApplicationController inertia_config(encrypt_history: true) # ... end ``` ## Clearing History To clear the history state on the server side, you can pass the `clear_history` option to the `render` method. ```ruby render inertia: {}, clear_history: true ``` Once the response has rendered on the client, the encryption key will be rotated, rendering the previous history state unreadable. You can also clear history on the client site by calling `router.clearHistory()`. --- --- url: /guide/how-it-works.md --- # How it works ## Use the Tools You Love With Inertia you build applications just like you've always done with your server-side web framework of choice. You use your framework's existing functionality for routing, controllers, middleware, authentication, authorization, data fetching, and more. However, Inertia replaces your application's view layer. Instead of using server-side rendering via PHP or Ruby templates, the views returned by your application are JavaScript page components. This allows you to build your entire frontend using React, Vue, or Svelte, while still enjoying the productivity of Laravel or your preferred server-side framework. ## Intercepting Requests As you might expect, simply creating your frontend in JavaScript doesn't give you a single-page application experience. If you were to click a link, your browser would make a full page visit, which would then cause your client-side framework to reboot on the subsequent page load. This is where Inertia changes everything. At its core, Inertia is essentially a client-side routing library. It allows you to make page visits without forcing a full page reload. This is done using the `` component, a light-weight wrapper around a normal anchor link. When you click an Inertia link, Inertia intercepts the click and makes the visit via XHR instead. You can even make these visits programmatically in JavaScript using `router.visit()`. When Inertia makes an XHR visit, the server detects that it's an Inertia visit and, instead of returning a full HTML response, it returns a JSON response with the JavaScript page component name and data (props). Inertia then dynamically swaps out the previous page component with the new page component and updates the browser's history state. **The end result is a silky smooth single-page experience.** To learn more about the nitty-gritty, technical details of how Inertia works under the hood, check out the [protocol page](/guide/the-protocol). --- --- url: /guide/http-requests.md --- # HTTP Requests @available\_since core=3.0.0 Not every request needs to trigger an Inertia page visit. For calls to an external API or fetching data from a non-Inertia endpoint, the `useHttp` hook provides the same developer experience as `useForm`, but for standalone HTTP requests. ## Basic Usage The `useHttp` hook accepts initial data and returns reactive state along with methods for making HTTP requests. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { useHttp } from '@inertiajs/react' export default function Search() { const { data, setData, get, processing } = useHttp({ query: '', }) function search(e) { setData('query', e.target.value) get('/api/search', { onSuccess: (response) => { console.log(response) }, }) } return ( <> {processing &&
Searching...
} ) } ``` \== Svelte ```svelte {#if http.processing}
Searching...
{/if} ``` ::: Unlike router visits, `useHttp` requests do not trigger page navigation or interact with Inertia's page lifecycle. They are plain HTTP requests that return JSON responses. ## Submitting Data The hook provides `get`, `post`, `put`, `patch`, and `delete` convenience methods. A generic `submit` method is also available for dynamic HTTP methods. :::tabs key:frameworks \== Vue ```js http.get(url, options) http.post(url, options) http.put(url, options) http.patch(url, options) http.delete(url, options) http.submit(method, url, options) ``` \== React ```js const { get, post, put, patch, delete: destroy, submit, } = useHttp({ /*...*/ }) get(url, options) post(url, options) put(url, options) patch(url, options) destroy(url, options) submit(method, url, options) ``` \== Svelte ```js http.get(url, options) http.post(url, options) http.put(url, options) http.patch(url, options) http.delete(url, options) http.submit(method, url, options) ``` ::: Each method returns a `Promise` that resolves with the parsed JSON response data. TypeScript users may [type the request data and response](/guide/typescript#http-helper) at the hook level or on a per-request basis. :::tabs key:frameworks \== Vue ```js const response = await http.post('/api/comments', { onError: (errors) => { console.log(errors) }, }) ``` \== React ```js const response = await post('/api/comments', { onError: (errors) => { console.log(errors) }, }) ``` \== Svelte ```js const response = await http.post('/api/comments', { onError: (errors) => { console.log(errors) }, }) ``` ::: ## Multiple Requests Each `useHttp` instance tracks its own `processing`, `errors`, and other reactive state. When making independent requests, you may create a separate instance for each one so their states don't collide. :::tabs key:frameworks \== Vue ```js const search = useHttp({ query: '' }) const upload = useHttp({ file: null }) ``` \== React ```js const search = useHttp({ query: '' }) const upload = useHttp({ file: null }) ``` \== Svelte ```js const search = useHttp({ query: '' }) const upload = useHttp({ file: null }) ``` ::: ## Reactive State The `useHttp` hook exposes the same reactive properties as `useForm`: | Property | Type | Description | | -------------------- | ---------------- | ------------------------------------------------- | | `errors` | `object` | Validation errors keyed by field name | | `hasErrors` | `boolean` | Whether validation errors exist | | `processing` | `boolean` | Whether a request is in progress | | `progress` | `object \| null` | Upload progress with `percentage` and `total` | | `wasSuccessful` | `boolean` | Whether the last request was successful | | `recentlySuccessful` | `boolean` | `true` for two seconds after a successful request | | `isDirty` | `boolean` | Whether the data differs from its defaults | ## Validation Errors When a request returns a `422` status code, the hook automatically parses validation errors and makes them available through the `errors` property. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { useHttp } from '@inertiajs/react' export default function CreateUser() { const { data, setData, post, errors, processing } = useHttp({ name: '', email: '', }) function save(e) { e.preventDefault() post('/api/users') } return ( setData('name', e.target.value)} /> {errors.name &&
{errors.name}
} setData('email', e.target.value)} /> {errors.email &&
{errors.email}
} ) } ``` \== Svelte ```svelte {#if http.errors.name}
{http.errors.name}
{/if} {#if http.errors.email}
{http.errors.email}
{/if} ``` ::: ## Displaying All Errors By default, validation errors are simplified to the first error message for each field. You may chain `withAllErrors()` to receive all error messages as arrays, which is useful for fields with multiple validation rules. :::tabs key:frameworks \== Vue ```js const http = useHttp({ name: '', email: '', }).withAllErrors() // http.errors.name === ['Name is required.', 'Name must be at least 3 characters.'] ``` \== React ```js const http = useHttp({ name: '', email: '', }).withAllErrors() // http.errors.name === ['Name is required.', 'Name must be at least 3 characters.'] ``` \== Svelte ```js const http = useHttp({ name: '', email: '', }).withAllErrors() // http.errors.name === ['Name is required.', 'Name must be at least 3 characters.'] ``` ::: The same method is available on the [`useForm`](/guide/forms#options-2) helper and the [`
`](/guide/forms#options) component. ## File Uploads When the data includes files, the hook automatically sends the request as `multipart/form-data`. Upload progress is available through the `progress` property. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { useHttp } from '@inertiajs/react' export default function Upload() { const { setData, post, progress, processing } = useHttp({ file: null, }) return ( <> setData('file', e.target.files[0])} /> {progress && } ) } ``` \== Svelte ```svelte (http.file = e.target.files[0])} /> {#if http.progress} {/if} ``` ::: ## Cancelling Requests You may cancel an in-progress request using the `cancel()` method. :::tabs key:frameworks \== Vue ```js http.cancel() ``` \== React ```js const { cancel } = useHttp({ /*...*/ }) cancel() ``` \== Svelte ```js http.cancel() ``` ::: ## Optimistic Updates The `useHttp` hook supports [optimistic updates](/guide/optimistic-updates) via the `optimistic()` method. The callback receives the current data and should return a partial update to apply immediately. :::tabs key:frameworks \== Vue ```js http .optimistic((data) => ({ likes: data.likes + 1, })) .post('/api/likes') ``` \== React ```js const { optimistic, post } = useHttp({ likes: 0 }) optimistic((data) => ({ likes: data.likes + 1, })) post('/api/likes') ``` \== Svelte ```js http .optimistic((data) => ({ likes: data.likes + 1, })) .post('/api/likes') ``` ::: The update is applied synchronously. If the request fails, the data is rolled back to its previous state. ## Event Callbacks Each submit method accepts an options object with lifecycle callbacks: ```js http.post('/api/users', { onBefore: () => { /*...*/ }, onStart: () => { /*...*/ }, onProgress: (progress) => { /*...*/ }, onSuccess: (response) => { /*...*/ }, onError: (errors) => { /*...*/ }, onCancel: () => { /*...*/ }, onFinish: () => { /*...*/ }, }) ``` You may return `false` from `onBefore` to cancel the request. ## Precognition The `useHttp` hook supports Precognition for real-time validation. Enable it by chaining `withPrecognition()` with the HTTP method and validation endpoint. :::tabs key:frameworks \== Vue ```js import { useHttp } from '@inertiajs/vue3' const http = useHttp({ name: '', email: '', }).withPrecognition('post', '/api/users') ``` \== React ```js import { useHttp } from '@inertiajs/react' const http = useHttp({ name: '', email: '', }).withPrecognition('post', '/api/users') ``` \== Svelte ```js import { useHttp } from '@inertiajs/svelte' const http = useHttp({ name: '', email: '', }).withPrecognition('post', '/api/users') ``` ::: Once enabled, the `validate()`, `touch()`, `touched()`, `valid()`, and `invalid()` methods become available, working identically to [form precognition](/guide/forms#precognition). ## History State You may persist data and errors in browser history state by providing a remember key as the first argument. :::tabs key:frameworks \== Vue ```js const http = useHttp('SearchData', { query: '', }) ``` \== React ```js const http = useHttp('SearchData', { query: '', }) ``` \== Svelte ```js const http = useHttp('SearchData', { query: '', }) ``` ::: You may exclude sensitive fields from being stored in history state using the `dontRemember()` method. ```js const http = useHttp('Login', { email: '', token: '', }).dontRemember('token') ``` --- --- url: /cookbook/inertia-modal.md --- # Inertia Modal [Inertia Modal](https://github.com/inertiaui/modal) is a powerful library that enables you to render any Inertia page as a modal dialog. It seamlessly integrates with your existing Inertia Rails application, allowing you to create modal workflows without the complexity of managing modal state manually. Here's a summary of the features: * Supports React and Vue * Zero backend configuration * Super simple frontend API * Support for Base Route / URL * Modal and slideover support * Headless support * Nested/stacked modals support * Reusable modals * Multiple sizes and positions * Reload props in modals * Easy communication between nested/stacked modals * Highly configurable While you can use Inertia Modal without changes on the backend, we recommend using the Rails gem [`inertia_rails-contrib`](https://github.com/skryukov/inertia_rails-contrib) to enhance your modals with base URL support. This ensures that your modals are accessible, SEO-friendly, and provide a better user experience. > \[!NOTE] > Svelte 5 is not yet supported by Inertia Modal. ## Installation ### 1. Install the NPM Package :::tabs key:frameworks \== Vue ```bash npm install @inertiaui/modal-vue ``` \== React ```bash npm install @inertiaui/modal-react ``` ::: ### 2. Configure Inertia Update your Inertia app setup to include the modal plugin: :::tabs key:frameworks \== Vue ```js // frontend/entrypoints/inertia.js import { createApp, h } from 'vue' import { createInertiaApp } from '@inertiajs/vue3' import { renderApp } from '@inertiaui/modal-vue' // [!code ++] createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('../pages/**/*.vue', { eager: true }) return pages[`../pages/${name}.vue`] }, setup({ el, App, props, plugin }) { createApp({ render: () => h(App, props) }) // [!code --] createApp({ render: renderApp(App, props) }) // [!code ++] .use(plugin) .mount(el) }, }) ``` \== React ```js // frontend/entrypoints/inertia.js import { createInertiaApp } from '@inertiajs/react' import { createElement } from 'react' // [!code --] import { renderApp } from '@inertiaui/modal-react' // [!code ++] import { createRoot } from 'react-dom/client' createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('../pages/**/*.jsx', { eager: true }) return pages[`../pages/${name}.jsx`] }, setup({ el, App, props }) { const root = createRoot(el) root.render(createElement(App, props)) // [!code --] root.render(renderApp(App, props)) // [!code ++] }, }) ``` ::: ### 3. Tailwind CSS Configuration :::tabs key:frameworks \== Vue For Tailwind CSS v4, add the modal styles to your CSS: ```css /* app/entrypoints/frontend/application.css */ @source '../../../node_modules/@inertiaui/modal-vue'; ``` For Tailwind CSS v3, update your `tailwind.config.js`: ```js export default { content: [ './node_modules/@inertiaui/modal-vue/src/**/*.{js,vue}', // other paths... ], } ``` \== React For Tailwind CSS v4, add the modal styles to your CSS: ```css /* app/entrypoints/frontend/application.css */ @source '../../../node_modules/@inertiaui/modal-react'; ``` For Tailwind CSS v3, update your `tailwind.config.js`: ```js export default { content: [ './node_modules/@inertiaui/modal-react/src/**/*.{js,jsx}', // other paths... ], } ``` ::: ### 4. Add the Ruby Gem (optional but recommended) Install the [`inertia_rails-contrib`](https://github.com/skryukov/inertia_rails-contrib) gem to your Rails application to enable base URL support for modals: ```bash bundle add inertia_rails-contrib ``` ## Basic example The package comes with two components: `Modal` and `ModalLink`. `ModalLink` is very similar to Inertia's [built-in `Link` component](/guide/links), but it opens the linked route in a modal instead of a full page load. So, if you have a link that you want to open in a modal, you can simply replace `Link` with `ModalLink`. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import {Link} from '@inertiajs/react' // [!code --] import {ModalLink} from '@inertiaui/modal-react' // [!code ++] export const CreateUserButton = () => { return ( Create User // [!code --] Create User // [!code ++] ) } ``` ::: The page you linked can then use the `Modal` component to wrap its content in a modal. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import {Modal} from '@inertiaui/modal-react' export const CreateUser = () => { return ( {/* [!code --] */} <> {/* [!code ++] */}

Create User

{/* Form fields */} {/* [!code --] */}
{/* [!code ++] */} ) } ``` ::: That's it! There is no need to change anything about your routes or controllers! ## Enhanced Usage With Base URL Support By default, Inertia Modal doesn't change the URL when opening a modal. It just stays on the same page and displays the modal content. However, you may want to change this behavior and update the URL when opening a modal. This has a few benefits: * It allows users to bookmark the modal and share the URL with others. * The modal becomes part of the browser history, so users can use the back and forward buttons. * It makes the modal content accessible to search engines (when using [SSR](/guide/server-side-rendering)). * It allows you to open the modal in a new tab. > \[!NOTE] > To enable this feature, you need to use the [`inertia_rails-contrib`](https://github.com/skryukov/inertia_rails-contrib) gem, which provides base URL support for modals. ## Define a Base Route To define the base route for your modal, you need to use the `inertia_modal` renderer in your controller instead of the `inertia` one. It accepts the same arguments as the `inertia` renderer: ```ruby class UsersController < ApplicationController def edit render inertia: { # [!code --] render inertia_modal: { # [!code ++] user:, roles: -> { Role.all }, } end end ``` Then, you can pass the `base_url` parameter to the `inertia_modal` renderer to define the base route for your modal: ```ruby class UsersController < ApplicationController def edit render inertia_modal: { user:, roles: -> { Role.all }, } # [!code --] }, base_url : users_path # [!code ++] end end ``` > \[!WARNING] Reusing the Modal URL with different Base Routes > The `base_url` parameter acts merely as a fallback when the modal is directly opened using a URL. If you open the > modal from a different route, the URL will be generated based on the current route. ## Open a Modal with a Base Route Finally, the frontend needs to know that we're using the browser history to navigate between modals. To do this, you need to add the `navigate` attribute to the `ModalLink` component: :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx export default function UserIndex() { return ( Create User ) } ``` ::: Now, when you click the "Create User" link, it will open the modal and update the URL to `/users/create`. ## Further Reading For advanced usage, configuration options, and additional features, check out [the official Inertia Modal documentation](https://inertiaui.com/inertia-modal/docs). --- --- url: /guide/starter-kits.md --- # Inertia Rails Starter Kits ## Overview Inertia Rails starter kits provide modern, full-stack scaffolding for building Rails applications with React, Vue, or Svelte frontends using Inertia.js. These starter kits are inspired by Laravel's starter kit ecosystem and offer the fastest way to begin building Inertia-powered Rails applications. ## Key Features * [Inertia Rails](https://inertia-rails.dev) & [Vite Rails](https://vite-ruby.netlify.app) setup * [React](https://react.dev) frontend with TypeScript & [shadcn/ui](https://ui.shadcn.com) component library * User authentication system (based on [Authentication Zero](https://github.com/lazaronixon/authentication-zero)) * [Kamal](https://kamal-deploy.org/) for deployment * Optional SSR support ## Available Starter Kits ### React Starter Kit **Repository:** [inertia-rails/react-starter-kit](https://github.com/inertia-rails/react-starter-kit) ### Vue Starter Kit **Repository:** [inertia-rails/vue-starter-kit](https://github.com/inertia-rails/vue-starter-kit) ### Svelte Starter Kit **Repository:** [inertia-rails/svelte-starter-kit](https://github.com/inertia-rails/svelte-starter-kit) ## Getting Started Each starter kit repository includes detailed setup instructions. The typical workflow: 1. Clone the desired starter kit repository 2. Run `bin/setup` --- --- url: /guide/infinite-scroll.md --- # Infinite Scroll Inertia's infinite scroll feature loads additional pages of content as users scroll, replacing traditional pagination controls. This is great for applications like chat interfaces, social feeds, photo grids, and product listings. ## Server-Side To configure your paginated data for infinite scrolling, you should use the `InertiaRails.scroll` method when returning your response. This method automatically configures the proper merge behavior and normalizes pagination metadata for the frontend component. :::tabs key:pagination\_gems \== Pagy ```ruby class UsersController < ApplicationController include Pagy::Method def index pagy, records = pagy(:countless, User.all) render inertia: { users: InertiaRails.scroll(pagy) { records.as_json(...) } } end end ``` \== Kaminari ```ruby class UsersController < ApplicationController def index users = User.page(params[:page]) render inertia: { # Pass collection to the scroll method to extract pagination metadata users: InertiaRails.scroll(users) { users.as_json(...) }, } end end ``` \== Manual ```ruby class UsersController < ApplicationController def index meta, users = paginate(User.order(:name)) render inertia: { users: InertiaRails.scroll(meta) { users.as_json(...) } } end private PER_PAGE = 20 def paginate(scope, page_param: :page) page = [params.fetch(page_param, 1).to_i, 1].max records = scope.offset((page - 1) * PER_PAGE).limit(PER_PAGE + 1) meta = { page_name: page_param.to_s, previous_page: page > 1 ? page - 1 : nil, next_page: records.length > PER_PAGE ? page + 1 : nil, current_page: page } [meta, records.first(PER_PAGE)] end end ``` ::: The `InertiaRails.scroll` method works with [Pagy](https://github.com/ddnexus/pagy) and [Kaminari](https://github.com/kaminari/kaminari) gems out of the box. For more details, check out the [`InertiaRails.scroll` method](#inertiarailsscroll-method) documentation. ## Client-Side On the client side, Inertia provides the `` component to automatically load additional pages of content. The component accepts a `data` prop that specifies the key of the prop containing your paginated data. The `` component should wrap the content that depends on the paginated data. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { InfiniteScroll } from '@inertiajs/react' export default function Users({ users }) { return ( {users.map((user) => (
{user.name}
))}
) } ``` \== Svelte ```svelte {#each users as user (user.id)}
{user.name}
{/each}
``` ::: The component uses [intersection observers](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to detect when users scroll near the end of the content and automatically triggers requests to load the next page. New data is merged with existing content rather than replacing it. ## Loading Buffer You can control how early content begins loading by setting a buffer distance. The buffer specifies how many pixels before the end of the content loading should begin. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx {/* ... */} ``` \== Svelte ```svelte ``` ::: In the example above, content will start loading 500 pixels before reaching the end of the current content. A larger buffer loads content earlier but potentially loads content that users may never see. ## URL Synchronization The infinite scroll component updates the browser URL's query string (`?page=...`) as users scroll through content. The URL reflects which page has the most visible items on screen, updating in both directions as users scroll up or down. This allows users to bookmark or share links to specific pages. You can disable this behavior to maintain the original page URL. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx {/* ... */} ``` \== Svelte ```svelte ``` ::: This is useful when infinite scroll is used for secondary content that shouldn't affect the main page URL, such as comments on a blog post or related products on a product page. ## Resetting When filters or other parameters change, you may need to reset the infinite scroll data to start from the beginning. Without resetting, new results will merge with existing content instead of replacing it. You can reset data using the `reset` visit option. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { InfiniteScroll, router } from '@inertiajs/react' export default function Users({ users }) { const show = (role) => { router.visit(route('users'), { data: { filter: { role } }, only: ['users'], reset: ['users'], }) } return ( <> {users.map((user) => (
{user.name}
))}
) } ``` \== Svelte ```svelte {#each users as user (user.id)}
{user.name}
{/each}
``` ::: For more information about the reset option, see the [Resetting props](/guide/merging-props#resetting-props) documentation. ## Loading Direction The infinite scroll component loads content in both directions when you scroll near the start or end. You can control this behavior using the `only-next` and `only-previous` props. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx /* Only load the next page */ export default () => ( {/* ... */} ) /* Only load the previous page */ export default () => ( {/* ... */} ) /* Load in both directions (default) */ export default () => ( {/* ... */} ) ``` \== Svelte ```svelte ``` ::: The default option is particularly useful when users start on a middle page and need to scroll in both directions to access all content. ## Reverse Mode For chat applications, timelines, or interfaces where content is sorted descendingly (newest items at the bottom), you can enable reverse mode. This configures the component to load older content when scrolling upward. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx {/* ... */} ``` \== Svelte ```svelte ``` ::: In reverse mode, the component flips the loading directions so that scrolling up loads the next page (older content) and scrolling down loads the previous page (newer content). The component handles the loading positioning, but you are responsible for reversing your content to display in the correct order. Reverse mode also enables automatic scrolling to the bottom on initial load, which you can disable with `:auto-scroll="false"``autoScroll={false}``auto-scroll={false}`. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx {/* ... */} ``` \== Svelte ```svelte ``` ::: ## Manual Mode Manual mode disables automatic loading when scrolling and allows you to control when content loads through the `next` and `previous` slots. For more details about available slot properties and customization options, see the [Slots](#slots) documentation. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { InfiniteScroll } from '@inertiajs/react' export default ({ users }) => ( hasMore && ( ) } next={({ loading, fetch, hasMore }) => hasMore && ( ) } > {users.map((user) => (
{user.name}
))}
) ``` \== Svelte ```svelte {#snippet previous({ hasMore, fetch, loading })} {#if hasMore} {/if} {/snippet} {#each users as user (user.id)}
{user.name}
{/each} {#snippet next({ hasMore, fetch, loading })} {#if hasMore} {/if} {/snippet}
``` ::: You can also configure the component to automatically switch to manual mode after a certain number of pages using the `:manual-after``manualAfter``manual-after` prop. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx {/* ... */} ``` \== Svelte ```svelte ``` ::: ## Slots The infinite scroll component provides several slots to customize the loading experience. These slots allow you to display custom loading indicators and create manual load controls. Each slot receives properties that provide loading state information and functions to trigger content loading. ### Default Slot The main content area where you render your data items. This slot receives loading state information. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx {({ loading, loadingPrevious, loadingNext }) => (
{/* Your content with access to loading states */}
)}
``` \== Svelte ```svelte {#snippet children({ loading, loadingPrevious, loadingNext })} {/snippet} ``` ::: ### Loading Slot The loading slot is used as a fallback when loading content and no custom `before` or `after` slots are provided. This creates a default loading indicator. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx 'Loading more users...'}> {/* Your content */} ``` \== Svelte ```svelte {#snippet loading()} Loading more users... {/snippet} ``` ::: ### Previous and Next Slots The `previous` and `next` slots are rendered above and below the main content, typically used for manual load controls. These slots receive several properties including loading states, fetch functions, and mode indicators. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { InfiniteScroll } from '@inertiajs/react' export default ({ users }) => ( manualMode && hasMore && ( ) } next={({ loading, fetch, hasMore, manualMode }) => manualMode && hasMore && ( ) } > {users.map((user) => (
{user.name}
))}
) ``` \== Svelte ```svelte {#snippet previous({ hasMore, fetch, loading, manualMode })} {#if manualMode && hasMore} {/if} {/snippet} {#each users as user (user.id)}
{user.name}
{/each} {#snippet next({ hasMore, fetch, loading, manualMode })} {#if manualMode && hasMore} {/if} {/snippet}
``` ::: The `loading`, `previous`, and `next` slots receive the following properties: | Property | Description | | :---------------- | :--------------------------------------------- | | `loading` | Whether the slot is currently loading content | | `loadingPrevious` | Whether previous content is loading | | `loadingNext` | Whether next content is loading | | `fetch` | Function to trigger loading for the slot | | `hasMore` | Whether more content is available for the slot | | `hasPrevious` | Whether more previous content is available | | `hasNext` | Whether more next content is available | | `manualMode` | Whether manual mode is active | | `autoMode` | Whether automatic loading is active | ## Custom Element The `InfiniteScroll` component renders as a `
` element. You may customize this to use any HTML element using the `as` prop. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx {products.map((product) => (
  • {product.name}
  • ))}
    ``` \== Svelte ```svelte {#each products as product (product.id)}
  • {product.name}
  • {/each}
    ``` ::: ## Element Targeting The infinite scroll component automatically tracks content and assigns page numbers to elements for [URL synchronization](#url-synchronization). When your data items are not direct children of the component's root element, you need to specify which element contains the actual data items using the `items-element``itemsElement``items-element` prop. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx {users.map((user) => ( ))}
    Name
    {user.name}
    ``` \== Svelte ```svelte {#each users as user (user.id)} {/each}
    Name
    {user.name}
    ``` ::: In this example, the component monitors the `#table-body` element and automatically tags each `` with a page number as new content loads. This enables proper URL updates based on which page's content is most visible in the viewport. You can also specify custom trigger elements for loading more content using CSS selectors. This prevents the default trigger elements from being rendered and uses intersection observers on your custom elements instead. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx {users.map((user) => ( ))}
    Name
    {user.name}
    Footer
    ``` \== Svelte ```svelte {#each users as user (user.id)} {/each}
    Name
    {user.name}
    Footer
    ``` ::: Alternatively, you can use template refs instead of CSS selectors. This avoids adding HTML attributes and provides direct element references. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { useRef } from 'react' export default ({ users }) => { const tableHeader = useRef() const tableFooter = useRef() const tableBody = useRef() return ( tableBody.current} startElement={() => tableHeader.current} endElement={() => tableFooter.current} > {users.map((user) => ( ))}
    Name
    {user.name}
    Footer
    ) } ``` \== Svelte ```svelte tableBody} start-element={() => tableHeader} end-element={() => tableFooter} > {#each users as user (user.id)} {/each}
    Name
    {user.name}
    Footer
    ``` ::: ## Scroll Containers The infinite scroll component works within any scrollable container, not just the main document. The component automatically adapts to use the custom scroll container for trigger detection and calculations instead of the main document scroll. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx
    {users.map((user) => (
    {user.name}
    ))}
    ``` \== Svelte ```svelte
    {#each users as user (user.id)}
    {user.name}
    {/each}
    ``` ::: ### Multiple Scroll Containers Sometimes you may need to render multiple infinite scroll components on a single page. However, if both components use the default `page` query parameter for [URL synchronization](#url-synchronization), they will conflict with each other. To resolve this, instruct each paginator to use a custom `page_name`. :::tabs key:pagination\_gems \== Pagy ```ruby class DashboardController < ApplicationController include Pagy::Method def index pagy_users, users = pagy(:countless, User.all, page_param: :users) pagy_orders, orders = pagy(:countless, Order.all, page_param: :orders) render inertia: { users: InertiaRails.scroll(pagy_users) { users.as_json(...) }, orders: InertiaRails.scroll(pagy_orders) { orders.as_json(...) } } end end ``` \== Kaminari ```ruby class DashboardController < ApplicationController def index users = User.page(params[:users]) orders = Order.page(params[:orders]) render inertia: { users: InertiaRails.scroll(users, page_name: 'users') { users.as_json(...) }, orders: InertiaRails.scroll(orders, page_name: 'orders') { orders.as_json(...) } } end end ``` \== Manual ```ruby class DashboardController < ApplicationController def index users_meta, users = paginate(User.order(:name), page_name: 'users') orders_meta, orders = paginate(Order.order(:created_at), page_name: 'orders') render inertia: { users: InertiaRails.scroll(users_meta) { users.as_json(...) }, orders: InertiaRails.scroll(orders_meta) { orders.as_json(...) } } end private PER_PAGE = 20 def paginate(scope, page_param: :page) page = [params.fetch(page_param, 1).to_i, 1].max records = scope.offset((page - 1) * PER_PAGE).limit(PER_PAGE + 1) meta = { page_name: page_param.to_s, previous_page: page > 1 ? page - 1 : nil, next_page: records.length > PER_PAGE ? page + 1 : nil, current_page: page } [meta, records.first(PER_PAGE)] end end ``` ::: The `InertiaRails.scroll` method automatically detects the `page_name` from each paginator metadata, allowing both scroll containers to maintain independent pagination state. This results in URLs like `?users=2&orders=3` instead of conflicting `?page=` parameters. ## Programmatic Access When you need to trigger loading actions programmatically, you may use a template ref. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { InfiniteScroll } from '@inertiajs/react' import { useRef } from 'react' export default ({ users }) => { const infiniteScrollRef = useRef(null) const fetchNext = () => { infiniteScrollRef.current?.fetchNext() } return ( <> {users.data.map((user) => (
    {user.name}
    ))}
    ) } ``` \== Svelte ```svelte {#each users as user (user.id)}
    {user.name}
    {/each}
    ``` ::: The component exposes the following methods: * `fetchNext()` - Manually fetch the next page * `fetchPrevious()` - Manually fetch the previous page * `hasNext()` - Whether there is a next page * `hasPrevious()` - Whether there is a previous page ## `InertiaRails.scroll` method The `InertiaRails.scroll` method provides server-side configuration for infinite scrolling. It automatically configures the proper merge behavior so that new data is appended or prepended to existing content instead of replacing it, and normalizes pagination metadata for the frontend component. ```ruby # Works with Pagy... InertiaRails.scroll(pagy_instance) { records.as_json(...) } # Works with Kaminari... InertiaRails.scroll(kaminari_collection) { kaminari_collection.as_json(...) } # Works with hash metadata... InertiaRails.scroll(metadata_hash) { data.as_json(...) } ``` If you don't use Pagy or Kaminari, or need custom pagination behavior, you may use the additional options that `scroll()` accepts. ### Hash Metadata When using custom pagination libraries or manual pagination, you can provide pagination metadata as a hash: ```ruby class UsersController < ApplicationController def index page = params[:page]&.to_i || 1 users = User.offset((page - 1) * 20).limit(21) has_more = users.count > 20 metadata = { page_name: 'page', current_page: page, previous_page: page > 1 ? page - 1 : nil, next_page: has_more ? page + 1 : nil } render inertia: { users: InertiaRails.scroll(metadata) { users.first(20).as_json(...) } } end end ``` The hash must include all required keys: `page_name`, `current_page`, `previous_page`, and `next_page`. ### Custom Pagination Adapters If you're using a pagination library that isn't supported out of the box, you can create and register a custom adapter: ```ruby class CustomPaginatorAdapter def match?(metadata) metadata.is_a?(CustomPaginator) end def call(metadata, **options) { page_name: options[:page_name] || 'page', previous_page: metadata.has_previous? ? metadata.previous_page_number : nil, next_page: metadata.has_next? ? metadata.next_page_number : nil, current_page: metadata.current_page_number } end end # Register the adapter (typically in an initializer) InertiaRails::ScrollMetadata.register_adapter(CustomPaginatorAdapter) ``` Adapters are checked in reverse registration order, so custom adapters registered later will take precedence over built-in adapters. ### Overriding Attributes You can override any of the default attributes by passing a hash of options. ```ruby class UsersController < ApplicationController def index users = User.page(params[:page]) render inertia: { users: InertiaRails.scroll(users, page_name: 'page_number') do users.as_json(...) end } end end ``` ### Wrapper Option The `wrapper` option allows you to specify a custom key for nested data structures. This is useful when your data is wrapped in an object with metadata: ```ruby class UsersController < ApplicationController def index users = User.page(params[:page]) render inertia: { users: InertiaRails.scroll(users, wrapper: 'data') do { items: users.as_json(...), metadata: { total: users.total_count } } end } end end ``` This example demonstrates how the `wrapper` option works with nested data structures, ensuring that only the `items` array gets merged during infinite scrolling while preserving the `metadata` object. --- --- url: /guide/instant-visits.md --- # Instant Visits @available\_since core=3.0.0 Sometimes you may wish to navigate to a new page without waiting for the server to respond. Instant visits allow Inertia to immediately swap to the target page component while the server request happens in the background. Once the server responds, the real props are merged in. Unlike [client-side visits](/guide/manual-visits#client-side-visits), which update the page entirely on the client without making a server request, instant visits still make a full server request. The difference is that the user sees the target page right away instead of waiting for the response. ## Basic Usage To make an instant visit, provide the target `component` name to a `Link` or to `router.visit()`. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( Dashboard ) ``` \== Svelte ```svelte Dashboard Dashboard ``` ::: When clicked, Inertia immediately renders the `Dashboard` component while the server request fires in the background. The full props are merged in when the response arrives. The target component must be able to render without its page-specific props, as only [shared props](/guide/shared-data) are available on the intermediate page. You may use optional chaining or conditional rendering to handle missing props. Programmatic instant visits work the same way via the `component` option on `router.visit()`. ```js router.visit('/dashboard', { component: 'Dashboard', }) ``` ## Shared Props The Rails adapter includes a `sharedProps` metadata key in the page response, listing the top-level prop keys registered via `shared_props`. ```json lines { "component": "Dashboard", "props": { "auth": { "user": "..." }, "stats": { /*...*/ } }, "sharedProps": ["auth"] } ``` Inertia reads this list and carries those props over from the current page to the intermediate page. Props like `auth` are available immediately, while page-specific props like `stats` will be `undefined` until the server responds. ## Page Props You may provide props for the intermediate page using the `pageProps` option. This is useful for passing data you already have on the current page, or for setting placeholder values to display loading states while the server responds. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( View Post ) ``` \== Svelte ```svelte View Post ``` ::: When `pageProps` is provided as an object, shared props are not automatically carried over. You are in full control of the intermediate page's props. A callback may also be passed to `pageProps`. The callback receives the current page's props and the shared props as arguments, so you may selectively spread them. ```js router.visit('/posts/1', { component: 'Posts/Show', pageProps: (currentProps, sharedProps) => ({ ...sharedProps, title: 'Loading...', }), }) ``` ## Disabling Shared Prop Keys You may disable the `sharedProps` metadata key in your configuration. The server will still resolve and include shared prop values in the response, but the metadata listing which keys are shared will be omitted. Without this list, the client cannot identify which props to carry over during instant visits. ```ruby # config/initializers/inertia.rb InertiaRails.configure do |config| config.expose_shared_prop_keys = false end ``` --- --- url: /cookbook/integrating-shadcn-ui.md --- # Integrating `shadcn/ui` This guide demonstrates how to integrate [shadcn/ui](https://ui.shadcn.com) - a collection of reusable React components - with your Inertia Rails application. ## Getting Started in 5 Minutes If you're starting fresh, create a new Rails application with Inertia (or skip this step if you already have one): :::tabs key:languages \== TypeScript ```bash rails new -JA shadcn-inertia-rails cd shadcn-inertia-rails bundle add inertia_rails rails generate inertia:install --framework=react --typescript --vite --tailwind --no-interactive Installing Inertia's Rails adapter ... ``` \== JavaScript ```bash rails new -JA shadcn-inertia-rails cd shadcn-inertia-rails bundle add inertia_rails rails generate inertia:install --framework=react --vite --tailwind --no-interactive Installing Inertia's Rails adapter ... ``` ::: > \[!NOTE] > You can also run `rails generate inertia:install` to run the installer interactively. > Need more details on the initial setup? Check out our [server-side setup guide](/guide/server-side-setup.md). ## Setting Up Path Aliases Let's configure our project to work seamlessly with `shadcn/ui`. Choose your path based on whether you're using TypeScript or JavaScript. :::tabs key:languages \== TypeScript You'll need to configure two files. First, update your `tsconfig.app.json`: ```json lines { "compilerOptions": { // ... "baseUrl": ".", "paths": { "@/*": ["./app/frontend/*"] } } // ... } ``` Then, set up your `tsconfig.json` to match `shadcn/ui`'s requirements (note the `baseUrl` and `paths` properties are different from the `tsconfig.app.json`): ```json lines { //... "compilerOptions": { /* Required for shadcn-ui/ui */ "baseUrl": "./app/frontend", "paths": { "@/*": ["./*"] } } } ``` \== JavaScript Using JavaScript? It's even simpler! Just create a `jsconfig.json`: ```json { "compilerOptions": { "baseUrl": "./app/frontend", "paths": { "@/*": ["./*"] } } } ``` ::: ## Initializing `shadcn/ui` Now you can initialize `shadcn/ui` with a single command: ```bash npx shadcn@latest init ✔ Preflight checks. ✔ Verifying framework. Found Vite. ✔ Validating Tailwind CSS. ✔ Validating import alias. ✔ Which style would you like to use? › New York ✔ Which color would you like to use as the base color? › Neutral ✔ Would you like to use CSS variables for theming? … no / yes ✔ Writing components.json. ✔ Checking registry. ✔ Updating tailwind.config.js ✔ Updating app/frontend/entrypoints/application.css ✔ Installing dependencies. ✔ Created 1 file: - app/frontend/lib/utils.js Success! Project initialization completed. You may now add components. ``` You're all set! Want to try it out? Add your first component: ```shell npx shadcn@latest add button ``` Now you can import and use your new button component from `@/components/ui/button`. Happy coding! > \[!NOTE] > Check out the [`shadcn/ui` components gallery](https://ui.shadcn.com/docs/components/accordion) to explore all the beautiful components at your disposal. ## Troubleshooting If you're using `vite` and see this error `No Tailwind CSS configuration found at path....` (but do have a `tailwind.config.js`) ensure you've imported the CSS properly. ``` @tailwind base; @tailwind components; @tailwind utilities; ``` Reference: [Link to Common Github Issue](https://github.com/shadcn-ui/ui/issues/4677) --- --- url: /guide.md --- # Introduction Welcome to the documentation for [inertia\_rails](https://github.com/inertiajs/inertia-rails) adapter for [Ruby on Rails](https://rubyonrails.org/) and [Inertia.js](https://inertiajs.com/). ## Why adapter-specific documentation? The [official documentation for Inertia.js](https://inertiajs.com) is great, but it's not Rails-specific anymore (see the [legacy docs](https://legacy.inertiajs.com)). This documentation aims to fill in the gaps and provide Rails-specific examples and explanations. ## JavaScript apps the monolith way Inertia is a new approach to building classic server-driven web apps. We call it the modern monolith. Inertia allows you to create fully client-side rendered, single-page apps, without the complexity that comes with modern SPAs. It does this by leveraging existing server-side patterns that you already love. Inertia has no client-side routing, nor does it require an API. Simply build controllers and page views like you've always done! Inertia works great with any backend framework — it was fine-tuned for [Laravel](https://laravel.com), so naturally we had to fine-tune it for [Ruby on Rails](https://rubyonrails.org/) too. ## Not a Framework Inertia isn't a framework, nor is it a replacement for your existing server-side or client-side frameworks. Rather, it's designed to work with them. Think of Inertia as glue that connects the two. Inertia does this via adapters. We currently have three official client-side adapters (React, Vue, and Svelte) and four server-side adapters (Laravel, Rails, Phoenix, and Django). ## Support Policy > \[!NOTE] > The `inertia_rails` gem has been on 3.x for years — long before Inertia.js itself reached 3.x. When Inertia.js 4.x arrives, we'll bump the gem to 4.x to align major version numbers and this policy going forward. Inertia.js follows the same support policy as [Laravel](https://laravel.com/docs/releases#support-policy). When a new major version is released, the previous version receives bug fixes for 6 months and security fixes for 12 months. Inertia.js v3 was released on March 26, 2026. | Version | Bug Fixes Until | Security Fixes Until | | ------- | ------------------ | -------------------- | | 0.x | End of life | End of life | | 1.x | End of life | End of life | | 2.x | September 26, 2026 | March 26, 2027 | | 3.x | TBD | TBD | ## Next Steps Want to learn a bit more before diving in? Check out the [who is it for](/guide/who-is-it-for) and [how it works](/guide/how-it-works) pages. Or, if you're ready to get started, jump right into the [installation instructions](/guide/server-side-setup). --- --- url: /guide/layouts.md --- # Layouts Most applications share common UI elements across pages, such as a primary navigation bar, sidebar, or footer. Layout components let you define this shared UI once and wrap your pages with it automatically. ## Creating Layouts A layout is a standard component that accepts child content. There is nothing Inertia-specific about it. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default function Layout({ children }) { return (
    Home About Contact
    {children}
    ) } ``` \== Svelte ```svelte
    Home About Contact
    {@render children()}
    ``` ::: You may use a layout by wrapping your page content with it directly. However, this approach forces the layout instance to be destroyed and recreated between visits. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import Layout from './Layout' export default function Welcome({ user }) { return (

    Welcome

    Hello {user.name}, welcome to your first Inertia app!

    ) } ``` \== Svelte ```svelte

    Welcome

    Hello {user.name}, welcome to your first Inertia app!

    ``` ::: ## Persistent Layouts Wrapping a page with a layout as a child component works, but it means the layout is destroyed and recreated on every visit. This prevents maintaining layout state across navigations, such as an audio player that should keep playing or a sidebar that should retain its scroll position. Persistent layouts solve this by telling Inertia which layout to use for a page. Inertia then manages the layout instance separately, keeping it alive between visits. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import Layout from './Layout' const Welcome = ({ user }) => { return ( <>

    Welcome

    Hello {user.name}, welcome to your first Inertia app!

    ) } Welcome.layout = Layout export default Welcome ``` \== Svelte ```svelte

    Welcome

    Hello {user.name}, welcome to your first Inertia app!

    ``` ::: Vue 3.3+ users may alternatively use [defineOptions](https://vuejs.org/api/sfc-script-setup.html#defineoptions) to define a layout within ` ``` ### Nested Layouts You may create more complex layout arrangements using nested layouts. Pass an array of layout components to wrap the page in multiple layers. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import SiteLayout from './SiteLayout' import NestedLayout from './NestedLayout' const Welcome = ({ user }) => { return ( <>

    Welcome

    Hello {user.name}, welcome to your first Inertia app!

    ) } Welcome.layout = [SiteLayout, NestedLayout] export default Welcome ``` \== Svelte ```svelte

    Welcome

    Hello {user.name}, welcome to your first Inertia app!

    ``` ::: ## Default Layouts @available\_since core=3.0.0 The `layout` option in `createInertiaApp` lets you define a default layout for all pages, saving you from defining it on every page individually. Per-page layouts always take precedence over the default. ```js import Layout from './Layout' createInertiaApp({ layout: () => Layout, // ... }) ``` You may also conditionally return a layout based on the page name. For example, you may wish to exclude public pages from the default layout. ```js import Layout from './Layout' createInertiaApp({ layout: (name) => { if (name.startsWith('Public/')) { return null } return Layout }, // ... }) ``` The full page object is also available as the second argument, giving you access to the page's URL, props, and other metadata. The `layout` callback supports all layout formats, including arrays for [nested layouts](#nested-layouts), named objects for [named layouts](#targeting-named-layouts), and tuples for [static props](#static-props). ### Using the Resolve Callback You may also set a default layout inside the `resolve` callback by mutating the resolved page component. The callback receives the component name and the full page object, which is useful when you need to conditionally apply layouts based on page data. :::tabs key:frameworks \== Vue ```js import Layout from './Layout' createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('../pages/**/*.vue', { eager: true }) let page = pages[`../pages/${name}.vue`] page.default.layout = page.default.layout || Layout return page }, // ... }) ``` \== React ```js import Layout from './Layout' createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('../pages/**/*.jsx', { eager: true }) let page = pages[`../pages/${name}.jsx`] page.default.layout = page.default.layout || Layout return page }, // ... }) ``` \== Svelte ```js import Layout from './Layout' createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('../pages/**/*.svelte', { eager: true }) let page = pages[`../pages/${name}.svelte`] return { default: page.default, layout: page.layout || Layout } }, // ... }) ``` ::: ## Layout Props Persistent layouts often need dynamic data from the current page, such as a page title, the active navigation item, or a sidebar toggle. Layout props provide a way to define defaults in your layout and override them from any page. ### Defining Defaults Layout props are defined as regular component props with default values. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx export default function Layout({ title = 'My App', showSidebar = true, children, }) { return ( <>
    {title}
    {showSidebar && }
    {children}
    ) } ``` \== Svelte ```svelte
    {title}
    {#if showSidebar} {/if}
    {@render children()}
    ``` ::: ### Static Props You may pass static props directly in your persistent layout definition using a tuple. These props are set once when the layout is defined and don't change between page navigations. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import Layout from './Layout' const Dashboard = ({ user }) => { return

    Dashboard

    } Dashboard.layout = [Layout, { title: 'Dashboard' }] export default Dashboard ``` \== Svelte ```svelte

    Dashboard

    ``` ::: ### Callback Props Sometimes layout props need to be derived from the current page's props. A callback function receives the page props and returns a layout definition with computed static props. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import Layout from './Layout' const Profile = () => { return

    Profile

    } Profile.layout = (props) => [ Layout, { title: 'Profile: ' + props.auth.user.name }, ] export default Profile ``` \== Svelte ```svelte

    Profile

    ``` ::: The callback receives the page's props and may return any valid layout format: a single component, a tuple with static props, an array for nested layouts, or a named layout object. TypeScript users may use the [`LayoutCallback`](/guide/typescript#layout-callbacks) type for type safety. #### Returning Props Only When a [default layout](#default-layouts) is configured in `createInertiaApp`, callbacks may return a plain props object instead of a full layout definition. Inertia will automatically use the default layout and merge the returned props onto it. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx const Profile = () => { return

    Profile

    } Profile.layout = (props) => ({ title: 'Profile: ' + props.auth.user.name, showSidebar: false, }) export default Profile ``` \== Svelte ```svelte

    Profile

    ``` ::: A static object may also be used when the props don't depend on page data. ```js Dashboard.layout = { title: 'Dashboard', showSidebar: true } ``` ### Dynamic Props You may also update layout props dynamically from any page component using the `setLayoutProps` function. TypeScript users may [type these props](/guide/typescript#layout-props) globally. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { setLayoutProps } from '@inertiajs/react' export default function Dashboard() { setLayoutProps({ title: 'Dashboard', showSidebar: false, }) return

    Dashboard

    } ``` \== Svelte ```svelte

    Dashboard

    ``` ::: ### Targeting Named Layouts [Nested layouts](#nested-layouts) may also be defined as a named object instead of an array, allowing you to target specific layouts with props. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import AppLayout from './AppLayout' import ContentLayout from './ContentLayout' Dashboard.layout = { app: AppLayout, content: ContentLayout, } ``` \== Svelte ```svelte ``` ::: You may target a specific named layout by passing the layout name as the first argument to `setLayoutProps`. :::tabs key:frameworks \== Vue ```js import { setLayoutProps } from '@inertiajs/vue3' setLayoutProps('sidebar', { collapsed: true, }) ``` \== React ```js import { setLayoutProps } from '@inertiajs/react' setLayoutProps('sidebar', { collapsed: true, }) ``` \== Svelte ```js import { setLayoutProps } from '@inertiajs/svelte' setLayoutProps('sidebar', { collapsed: true, }) ``` ::: [Nested layouts](#nested-layouts) and named layouts may also include static props using the tuple syntax. ```js // Nested layouts with static props Dashboard.layout = [ [AppLayout, { title: 'Dashboard' }], [ContentLayout, { padding: 'sm' }], ] // Named layouts with static props Dashboard.layout = { app: [AppLayout, { theme: 'dark' }], content: [ContentLayout, { padding: 'sm' }], } ``` ### Merge Priority Layout props are resolved from multiple sources with the following priority (highest to lowest): 1. **Dynamic props** - set via `setLayoutProps()` 2. **Static props** - defined in the persistent layout definition (including [callback props](#callback-props)) 3. **Defaults** - declared as default values on the layout component's props ### Auto-Reset on Navigation Dynamic layout props are automatically reset when navigating to a new page (unless `preserveState` is enabled). This ensures each page starts with a clean slate and only the layout props explicitly set by that page are applied. ### Resetting Props You may also manually reset all dynamic layout props using `resetLayoutProps`. :::tabs key:frameworks \== Vue ```js import { resetLayoutProps } from '@inertiajs/vue3' resetLayoutProps() ``` \== React ```js import { resetLayoutProps } from '@inertiajs/react' resetLayoutProps() ``` \== Svelte ```js import { resetLayoutProps } from '@inertiajs/svelte' resetLayoutProps() ``` ::: --- --- url: /guide/links.md --- # Links To create links to other pages within an Inertia app, you will typically use the Inertia `` component. This component is a light wrapper around a standard anchor `` link that intercepts click events and prevents full page reloads. This is [how Inertia provides a single-page app experience](/guide/how-it-works) once your application has been loaded. ## Creating Links To create an Inertia link, use the Inertia `` component. Any attributes you provide to this component will be proxied to the underlying HTML tag. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => Home ``` \== Svelte ```svelte Home Home ``` ::: By default, Inertia renders links as anchor `` elements. However, you can change the tag using the `as` prop. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( Logout ) // Renders as... // ``` \== Svelte ```svelte Logout ``` ::: > \[!NOTE] > Creating `POST/PUT/PATCH/DELETE` anchor `` links is discouraged as it causes "Open Link in New Tab / Window" accessibility issues. The component automatically renders a ` Logout ``` ::: ## Data When making `POST` or `PUT` requests, you may wish to add additional data to the request. You can accomplish this using the `data` prop. The provided data can be an `object` or `FormData` instance. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( Save ) ``` \== Svelte ```svelte Save ``` ::: ## Custom Headers The `headers` prop allows you to add custom headers to an Inertia link. However, the headers Inertia uses internally to communicate its state to the server take priority and therefore cannot be overwritten. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( Save ) ``` \== Svelte ```svelte Save ``` ::: ## Browser History The `replace` prop allows you to specify the browser's history behavior. By default, page visits push (new) state (`window.history.pushState`) into the history; however, it's also possible to replace state (`window.history.replaceState`) by setting the `replace` prop to `true`. This will cause the visit to replace the current history state instead of adding a new history state to the stack. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( Home ) ``` \== Svelte ```svelte Home Home ``` ::: ## State Preservation You can preserve a page component's local state using the `preserveState` prop. This will prevent a page component from fully re-rendering. The `preserveState` prop is especially helpful on pages that contain forms, since you can avoid manually repopulating input fields and can also maintain a focused input. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( <> Search ) ``` \== Svelte ```svelte Search ``` ::: ## Scroll Preservation You can use the `preserveScroll` prop to prevent Inertia from automatically resetting the scroll position when making a page visit. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( Home ) ``` \== Svelte ```svelte Home Home ``` ::: For more information on managing scroll position, check out the documentation on [scroll management](/guide/scroll-management). ## Partial Reloads The `only` prop allows you to specify that only a subset of a page's props (data) should be retrieved from the server on subsequent visits to that page. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( Show active ) ``` \== Svelte ```svelte Show active Show active ``` ::: For more information on this topic, check out the complete documentation on [partial reloads](/guide/partial-reloads). ## View Transitions You may enable [View transitions](/guide/view-transitions) for a link by setting the `viewTransition` prop to `true`. This will use the browser's View Transitions API to animate the page transition. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( Navigate ) ``` \== Svelte ```svelte Navigate ``` ::: ## Active States It's common to set an active state for navigation links based on the current page. This can be accomplished when using Inertia by inspecting the `page` object and doing string comparisons against the `page.url` and `page.component` properties. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { usePage } from '@inertiajs/react' export default () => { const { url, component } = usePage() return ( <> // URL exact match... Users // Component exact match... Users // URL starts with (/users, /users/create, /users/1, etc.)... Users // Component starts with (Users/Index, Users/Create, Users/Show, etc.)... Users ) } ``` \== Svelte ```svelte Users Users Users Users ``` ::: You can perform exact match comparisons (`===`), `startsWith()` comparisons (useful for matching a subset of pages), or even more complex comparisons using regular expressions. Using this approach, you're not limited to just setting class names. You can use this technique to conditionally render any markup on active state, such as different link text or even an SVG icon that represents the link is active. ## Data Loading Attribute While a link is making an active request, a `data-loading` attribute is added to the link element. This allows you to style the link while it's in a loading state. The attribute is removed once the request is complete. --- --- url: /guide/load-when-visible.md --- # Load When Visible Inertia supports lazy loading data on scroll using the Intersection Observer API. It provides the `WhenVisible` component as a convenient way to load data when an element becomes visible in the viewport. The `WhenVisible` component accepts a `data` prop that specifies the key of the prop to load. It also accepts a `fallback` prop that specifies a component to render while the data is loading. The `WhenVisible` component should wrap the component that depends on the data. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { WhenVisible } from '@inertiajs/react' export default () => (
    Loading...
    }>
    ) ``` \== Svelte ```svelte {#snippet fallback()}
    Loading...
    {/snippet} {#each permissions as permission} {/each}
    ``` ::: If you'd like to load multiple props when an element becomes visible, you can provide an array to the `data` prop. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { WhenVisible } from '@inertiajs/react' export default () => (
    Loading...
    }>
    ) ``` \== Svelte ```svelte {#snippet fallback()}
    Loading...
    {/snippet}
    ``` ::: ## Loading Before Visible If you'd like to start loading data before the element is visible, you can provide a value to the `buffer` prop. The buffer value is a number that represents the number of pixels before the element is visible. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { WhenVisible } from '@inertiajs/react' export default () => (
    Loading...
    } >
    ) ``` \== Svelte ```svelte {#snippet fallback()}
    Loading...
    {/snippet} {#each permissions as permission} {/each}
    ``` ::: In the above example, the data will start loading 500 pixels before the element is visible. By default, the `WhenVisible` component wraps the fallback template in a `div` element so it can ensure the element is visible in the viewport. If you want to customize the wrapper element, you can provide the `as` prop. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { WhenVisible } from '@inertiajs/react' export default () => ( ) ``` \== Svelte ```svelte ``` ::: ## Always Trigger By default, the `WhenVisible` component will only trigger once when the element becomes visible. If you want to always trigger the data loading when the element is visible, you can provide the `always` prop. This is useful when you want to load data every time the element becomes visible, such as when the element is at the end of an infinite scroll list and you want to load more data. Alternatively, you can use the [Infinite scroll](/guide/infinite-scroll) component which handles this use case for you. Note that if the data loading request is already in flight, the component will wait until it is finished to start the next request if the element is still visible in the viewport. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { WhenVisible } from '@inertiajs/react' export default () => ( ) ``` \== Svelte ```svelte ``` ::: ### Fetching State @available\_since core=2.3.2 The `WhenVisible` component exposes a `fetching` slot prop that you may use to display a loading indicator during subsequent requests. This is useful because the `fallback` is only shown on the initial load, while `fetching` allows you to indicate that data is being refreshed on subsequent loads. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { WhenVisible } from '@inertiajs/react' export default () => (
    Loading...
    }> {({ fetching }) => ( <> {fetching &&
    Refreshing...
    } )}
    ) ``` \== Svelte ```svelte {#snippet children({ fetching })} {#if fetching}
    Refreshing...
    {/if} {/snippet} {#snippet fallback()}
    Loading...
    {/snippet}
    ``` ::: ## Preserving Errors @available\_since core=3.0.0 The `WhenVisible` component sets `preserveErrors: true` by default, ensuring that validation errors are not cleared when it triggers a reload. You may override this via the `params` prop. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx {/* ... */} ``` \== Svelte ```svelte ``` ::: ## Form Submissions When submitting forms, you may want to use the `except` option to exclude the props that are being used by the `WhenVisible` component. This prevents the props from being reloaded when you get redirected back to the current page because of validation errors. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { useForm, WhenVisible } from '@inertiajs/react' export default function CreateUser() { const { data, setData, post } = useForm({ name: '', email: '', }) function submit(e) { e.preventDefault() post('/users', { except: ['permissions'], }) } return ( <>
    {/* ... */}
    {/* ... */} ) } ``` \== Svelte ```svelte
    ``` ::: --- --- url: /guide/manual-visits.md --- # Manual Visits In addition to [creating links](/guide/links), it's also possible to manually make Inertia visits / requests programmatically via JavaScript. This is accomplished via the `router.visit()` method. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.visit(url, { method: 'get', data: {}, replace: false, preserveState: false, preserveScroll: false, only: [], except: [], headers: {}, errorBag: null, forceFormData: false, queryStringArrayFormat: 'brackets', async: false, showProgress: true, fresh: false, reset: [], preserveUrl: false, prefetch: false, preserveErrors: false, viewTransition: false, component: null, pageProps: null, onCancelToken: (cancelToken) => {}, onCancel: () => {}, onBefore: (visit) => {}, onStart: (visit) => {}, onProgress: (progress) => {}, onSuccess: (page) => {}, onError: (errors) => {}, onHttpException: (response) => {}, onNetworkError: (error) => {}, onFinish: (visit) => {}, onPrefetching: () => {}, onPrefetched: () => {}, }) ``` \== React ```js import { router } from '@inertiajs/react' router.visit(url, { method: 'get', data: {}, replace: false, preserveState: false, preserveScroll: false, only: [], except: [], headers: {}, errorBag: null, forceFormData: false, queryStringArrayFormat: 'brackets', async: false, showProgress: true, fresh: false, reset: [], preserveUrl: false, prefetch: false, preserveErrors: false, viewTransition: false, component: null, pageProps: null, onCancelToken: (cancelToken) => {}, onCancel: () => {}, onBefore: (visit) => {}, onStart: (visit) => {}, onProgress: (progress) => {}, onSuccess: (page) => {}, onError: (errors) => {}, onHttpException: (response) => {}, onNetworkError: (error) => {}, onFinish: (visit) => {}, onPrefetching: () => {}, onPrefetched: () => {}, }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.visit(url, { method: 'get', data: {}, replace: false, preserveState: false, preserveScroll: false, only: [], except: [], headers: {}, errorBag: null, forceFormData: false, queryStringArrayFormat: 'brackets', async: false, showProgress: true, fresh: false, reset: [], preserveUrl: false, prefetch: false, preserveErrors: false, viewTransition: false, component: null, pageProps: null, onCancelToken: (cancelToken) => {}, onCancel: () => {}, onBefore: (visit) => {}, onStart: (visit) => {}, onProgress: (progress) => {}, onSuccess: (page) => {}, onError: (errors) => {}, onHttpException: (response) => {}, onNetworkError: (error) => {}, onFinish: (visit) => {}, onPrefetching: () => {}, onPrefetched: () => {}, }) ``` ::: However, it's generally more convenient to use one of Inertia's shortcut request methods. These methods share all the same options as `router.visit()`. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.get(url, data, options) router.post(url, data, options) router.put(url, data, options) router.patch(url, data, options) router.delete(url, options) router.reload(options) // Uses the current URL ``` \== React ```js import { router } from '@inertiajs/react' router.get(url, data, options) router.post(url, data, options) router.put(url, data, options) router.patch(url, data, options) router.delete(url, options) router.reload(options) // Uses the current URL ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.get(url, data, options) router.post(url, data, options) router.put(url, data, options) router.patch(url, data, options) router.delete(url, options) router.reload(options) // Uses the current URL ``` ::: The `reload()` method is a convenient, shorthand method that automatically visits the current page with `preserveState` and `preserveScroll` both set to `true`, making it the perfect method to invoke when you just want to reload the current page's data. ## Method When making manual visits, you may use the `method` option to set the request's HTTP method to `get`, `post`, `put`, `patch` or `delete`. The default method is `get`. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.visit(url, { method: 'post' }) ``` \== React ```js import { router } from '@inertiajs/react' router.visit(url, { method: 'post' }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.visit(url, { method: 'post' }) ``` ::: > \[!WARNING] > Uploading files via `put` or `patch` is not supported in Rails. Instead, make the request via `post`, including a `_method` attribute or a `X-HTTP-METHOD-OVERRIDE` header set to `put` or `patch`. For more info see [`Rack::MethodOverride`](https://github.com/rack/rack/blob/main/lib/rack/method_override.rb). ## Data You may use the `data` option to add data to the request. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.visit('/users', { method: 'post', data: { name: 'John Doe', email: 'john.doe@example.com', }, }) ``` \== React ```js import { router } from '@inertiajs/react' router.visit('/users', { method: 'post', data: { name: 'John Doe', email: 'john.doe@example.com', }, }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.visit('/users', { method: 'post', data: { name: 'John Doe', email: 'john.doe@example.com', }, }) ``` ::: For convenience, the `get()`, `post()`, `put()`, and `patch()` methods all accept `data` as their second argument. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.post('/users', { name: 'John Doe', email: 'john.doe@example.com', }) ``` \== React ```js import { router } from '@inertiajs/react' router.post('/users', { name: 'John Doe', email: 'john.doe@example.com', }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.post('/users', { name: 'John Doe', email: 'john.doe@example.com', }) ``` ::: ## Custom Headers The `headers` option allows you to add custom headers to a request. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.post('/users', data, { headers: { 'Custom-Header': 'value', }, }) ``` \== React ```js import { router } from '@inertiajs/react' router.post('/users', data, { headers: { 'Custom-Header': 'value', }, }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.post('/users', data, { headers: { 'Custom-Header': 'value', }, }) ``` ::: The headers Inertia uses internally to communicate its state to the server take priority and therefore cannot be overwritten. ## Global Visit Options You may configure a `visitOptions` callback when [initializing your Inertia app](/guide/client-side-setup#configuring-defaults) to modify visit options globally for every request. The callback receives the target URL and the current visit options, and should return an object with any options you want to override. :::tabs key:frameworks \== Vue ```js import { createApp, h } from 'vue' import { createInertiaApp } from '@inertiajs/vue3' createInertiaApp({ // ... defaults: { visitOptions: (href, options) => { return { headers: { ...options.headers, 'X-Custom-Header': 'value', }, } }, }, }) ``` \== React ```js import { createInertiaApp } from '@inertiajs/react' import { createRoot } from 'react-dom/client' createInertiaApp({ // ... defaults: { visitOptions: (href, options) => { return { headers: { ...options.headers, 'X-Custom-Header': 'value', }, } }, }, }) ``` \== Svelte ```js import { createInertiaApp } from '@inertiajs/svelte' createInertiaApp({ // ... defaults: { visitOptions: (href, options) => { return { headers: { ...options.headers, 'X-Custom-Header': 'value', }, } }, }, }) ``` ::: ## File Uploads When making visits / requests that include files, Inertia will automatically convert the request data into a `FormData` object. If you would like the request to always use a `FormData` object, you may use the `forceFormData` option. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.post('/companies', data, { forceFormData: true, }) ``` \== React ```js import { router } from '@inertiajs/react' router.post('/companies', data, { forceFormData: true, }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.post('/companies', data, { forceFormData: true, }) ``` ::: For more information on uploading files, check out the dedicated [file uploads](/guide/file-uploads) documentation. ## Browser History When making visits, Inertia automatically adds a new entry into the browser history. However, it's also possible to replace the current history entry by setting the `replace` option to `true`. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.get('/users', { search: 'John' }, { replace: true }) ``` \== React ```js import { router } from '@inertiajs/react' router.get('/users', { search: 'John' }, { replace: true }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.get('/users', { search: 'John' }, { replace: true }) ``` ::: Visits made to the same URL automatically set `replace` to `true`. ## Client Side Visits You can use the `router.push` and `router.replace` method to make client-side visits. This method is useful when you want to update the browser's history without making a server request. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.push({ url: '/users', component: 'Users', props: { search: 'John' }, clearHistory: false, encryptHistory: false, preserveScroll: false, preserveState: false, errorBag: null, onSuccess: (page) => {}, onError: (errors) => {}, onFinish: (visit) => {}, }) ``` \== React ```js import { router } from '@inertiajs/react' router.push({ url: '/users', component: 'Users', props: { search: 'John' }, clearHistory: false, encryptHistory: false, preserveScroll: false, preserveState: false, errorBag: null, onSuccess: (page) => {}, onError: (errors) => {}, onFinish: (visit) => {}, }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.push({ url: '/users', component: 'Users', props: { search: 'John' }, clearHistory: false, encryptHistory: false, preserveScroll: false, preserveState: false, errorBag: null, onSuccess: (page) => {}, onError: (errors) => {}, onFinish: (visit) => {}, }) ``` ::: All the parameters are optional. By default, all passed parameters (except `errorBag`) will be merged with the current page. This means you are responsible for overriding the current page's URL, component, and props. If you need access to the current page's props, you may pass a function to the `props` option. This function receives the current props (including any [once props](/guide/once-props)) and should return the new props. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.push({ url: '/users', component: 'Users' }) router.replace({ props: (currentProps) => ({ ...currentProps, search: 'John' }), }) ``` \== React ```js import { router } from '@inertiajs/react' router.push({ url: '/users', component: 'Users' }) router.replace({ props: (currentProps) => ({ ...currentProps, search: 'John' }), }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.push({ url: '/users', component: 'Users' }) router.replace({ props: (currentProps) => ({ ...currentProps, search: 'John' }), }) ``` ::: @available\_since core=2.3.10 The function also receives [once props](/guide/once-props) as a second argument. This is useful when you want to replace all regular props while still preserving once props. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.replace({ props: (currentProps, onceProps) => ({ ...onceProps, search: 'John' }), }) ``` \== React ```js import { router } from '@inertiajs/react' router.replace({ props: (currentProps, onceProps) => ({ ...onceProps, search: 'John' }), }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.replace({ props: (currentProps, onceProps) => ({ ...onceProps, search: 'John' }), }) ``` ::: The `errorBag` option allows you to specify which error bag to use when handling validation errors in the `onError` callback. Make sure that any route you push on the client side is also defined on the server side. If the user refreshes the page, the server will need to know how to render the page. > \[!WARNING] > Some browsers limit the number of `history.pushState()` and > `history.replaceState()` calls allowed within a short time period. Inertia > catches this error and logs it to the console, but the state update will be > lost. Avoid calling `router.push()` or `router.replace()` too frequently, and > consider debouncing or batching updates in high-frequency scenarios. ### Prop Helpers @available\_since core=2.2.0 Inertia provides three helper methods for updating page props without making server requests. These methods are shortcuts to `router.replace()` and automatically set `preserveScroll` and `preserveState` to `true`. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' // Replace a prop value... router.replaceProp('user.name', 'Jane Smith') // Append to an array prop... router.appendToProp('messages', { id: 4, text: 'New message' }) // Prepend to an array prop... router.prependToProp('tags', 'urgent') ``` \== React ```js import { router } from '@inertiajs/react' // Replace a prop value... router.replaceProp('user.name', 'Jane Smith') // Append to an array prop... router.appendToProp('messages', { id: 4, text: 'New message' }) // Prepend to an array prop... router.prependToProp('tags', 'urgent') ``` \== Svelte ```js import { router } from '@inertiajs/svelte' // Replace a prop value... router.replaceProp('user.name', 'Jane Smith') // Append to an array prop... router.appendToProp('messages', { id: 4, text: 'New message' }) // Prepend to an array prop... router.prependToProp('tags', 'urgent') ``` ::: All three methods support dot notation for nested props and can accept a callback function that receives the current value as the first argument and the current page props as the second argument. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.prependToProp('notifications', (current, props) => { return { id: Date.now(), message: `Hello ${props.user.name}`, } }) ``` \== React ```js import { router } from '@inertiajs/react' router.prependToProp('notifications', (current, props) => { return { id: Date.now(), message: `Hello ${props.user.name}`, } }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.prependToProp('notifications', (current, props) => { return { id: Date.now(), message: `Hello ${props.user.name}`, } }) ``` ::: ## State Preservation By default, page visits to the same page create a fresh page component instance. This causes any local state, such as form inputs, scroll positions, and focus states to be lost. However, in some situations, it's necessary to preserve the page component state. For example, when submitting a form, you need to preserve your form data in the event that form validation fails on the server. For this reason, the `post`, `put`, `patch`, `delete`, and `reload` methods all set the `preserveState` option to `true` by default. You can instruct Inertia to preserve the component's state when using the `get` method by setting the `preserveState` option to `true`. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.get('/users', { search: 'John' }, { preserveState: true }) ``` \== React ```js import { router } from '@inertiajs/react' router.get('/users', { search: 'John' }, { preserveState: true }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.get('/users', { search: 'John' }, { preserveState: true }) ``` ::: If you'd like to only preserve state if the response includes validation errors, set the `preserveState` option to "errors". :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.get('/users', { search: 'John' }, { preserveState: 'errors' }) ``` \== React ```js import { router } from '@inertiajs/react' router.get('/users', { search: 'John' }, { preserveState: 'errors' }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.get('/users', { search: 'John' }, { preserveState: 'errors' }) ``` ::: You can also lazily evaluate the `preserveState` option based on the response by providing a callback. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.post('/users', data, { preserveState: (page) => page.props.someProp === 'value', }) ``` \== React ```js import { router } from '@inertiajs/react' router.post('/users', data, { preserveState: (page) => page.props.someProp === 'value', }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.post('/users', data, { preserveState: (page) => page.props.someProp === 'value', }) ``` ::: ## Scroll Preservation When navigating between pages, Inertia mimics default browser behavior by automatically resetting the scroll position of the document body (as well as any [scroll regions](/guide/scroll-management#scroll-regions) you've defined) back to the top of the page. You can disable this behavior by setting the `preserveScroll` option to `true`. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.visit(url, { preserveScroll: true }) ``` \== React ```js import { router } from '@inertiajs/react' router.visit(url, { preserveScroll: true }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.visit(url, { preserveScroll: true }) ``` ::: If you'd like to only preserve the scroll position if the response includes validation errors, set the `preserveScroll` option to `"errors"`. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.visit(url, { preserveScroll: 'errors' }) ``` \== React ```js import { router } from '@inertiajs/react' router.visit(url, { preserveScroll: 'errors' }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.visit(url, { preserveScroll: 'errors' }) ``` ::: You can also lazily evaluate the `preserveScroll` option based on the response by providing a callback. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.post('/users', data, { preserveScroll: (page) => page.props.someProp === 'value', }) ``` \== React ```js import { router } from '@inertiajs/react' router.post('/users', data, { preserveScroll: (page) => page.props.someProp === 'value', }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.post('/users', data, { preserveScroll: (page) => page.props.someProp === 'value', }) ``` ::: For more information regarding this feature, check out the [scroll management](/guide/scroll-management) documentation. ## Partial Reloads The `only` option allows you to request a subset of the props (data) from the server on subsequent visits to the same page, thus making your application more efficient since it does not need to retrieve data that the page is not interested in refreshing. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.get('/users', { search: 'John' }, { only: ['users'] }) ``` \== React ```js import { router } from '@inertiajs/react' router.get('/users', { search: 'John' }, { only: ['users'] }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.get('/users', { search: 'John' }, { only: ['users'] }) ``` ::: For more information on this feature, check out the [partial reloads](/guide/partial-reloads) documentation. ## View Transitions You may enable [View transitions](/guide/view-transitions) for a visit by setting the `viewTransition` option to `true`. This will use the browser's View Transitions API to animate the page transition. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.visit('/another-page', { viewTransition: true }) ``` \== React ```js import { router } from '@inertiajs/react' router.visit('/another-page', { viewTransition: true }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.visit('/another-page', { viewTransition: true }) ``` ::: ## Visit Cancellation @available\_since core=3.0.0 You may cancel all in-flight visits using the `router.cancelAll()` method. By default, this cancels all synchronous, asynchronous, and prefetch requests. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' // Cancel all in-flight requests... router.cancelAll() ``` \== React ```js import { router } from '@inertiajs/react' // Cancel all in-flight requests... router.cancelAll() ``` \== Svelte ```js import { router } from '@inertiajs/svelte' // Cancel all in-flight requests... router.cancelAll() ``` ::: You may selectively cancel specific request types by passing an options object. ```js // Cancel only async requests (leaving sync and prefetch active)... router.cancelAll({ sync: false, prefetch: false }) // Cancel only sync requests... router.cancelAll({ async: false, prefetch: false }) // Cancel everything except prefetch requests... router.cancelAll({ prefetch: false }) ``` ### Cancel Tokens @available\_since core=0.3.0 For more granular control, you may cancel individual visits using a cancel token. Inertia automatically generates a cancel token and provides it via the `onCancelToken()` callback prior to making the visit. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.post('/users', data, { onCancelToken: (cancelToken) => (this.cancelToken = cancelToken), }) // Cancel the visit... this.cancelToken.cancel() ``` \== React ```js import { router } from '@inertiajs/react' router.post('/users', data, { onCancelToken: (cancelToken) => (this.cancelToken = cancelToken), }) // Cancel the visit... this.cancelToken.cancel() ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.post('/users', data, { onCancelToken: (cancelToken) => (this.cancelToken = cancelToken), }) // Cancel the visit... this.cancelToken.cancel() ``` ::: The `onCancel()` and `onFinish()` event callbacks will be executed when a visit is cancelled. ## Event Callbacks In addition to Inertia's [global events](/guide/events), Inertia also provides a number of per-visit event callbacks. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.post('/users', data, { onBefore: (visit) => {}, onStart: (visit) => {}, onProgress: (progress) => {}, onSuccess: (page) => {}, onError: (errors) => {}, onHttpException: (response) => {}, onNetworkError: (error) => {}, onCancel: () => {}, onFinish: (visit) => {}, onPrefetching: () => {}, onPrefetched: () => {}, }) ``` \== React ```js import { router } from '@inertiajs/react' router.post('/users', data, { onBefore: (visit) => {}, onStart: (visit) => {}, onProgress: (progress) => {}, onSuccess: (page) => {}, onError: (errors) => {}, onHttpException: (response) => {}, onNetworkError: (error) => {}, onCancel: () => {}, onFinish: (visit) => {}, onPrefetching: () => {}, onPrefetched: () => {}, }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.post('/users', data, { onBefore: (visit) => {}, onStart: (visit) => {}, onProgress: (progress) => {}, onSuccess: (page) => {}, onError: (errors) => {}, onHttpException: (response) => {}, onNetworkError: (error) => {}, onCancel: () => {}, onFinish: (visit) => {}, onPrefetching: () => {}, onPrefetched: () => {}, }) ``` ::: Returning `false` from the `onBefore()` callback will cause the visit to be canceled. Returning `false` from `onHttpException()` or `onNetworkError()` will prevent the corresponding [global event](/guide/events) from being fired and its default behavior. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.delete(`/users/${user.id}`, { onBefore: () => confirm('Are you sure you want to delete this user?'), }) ``` \== React ```js import { router } from '@inertiajs/react' router.delete(`/users/${user.id}`, { onBefore: () => confirm('Are you sure you want to delete this user?'), }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.delete(`/users/${user.id}`, { onBefore: () => confirm('Are you sure you want to delete this user?'), }) ``` ::: It's also possible to return a promise from the `onSuccess()` and `onError()` callbacks. When doing so, the "finish" event will be delayed until the promise has resolved. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.post(url, { onSuccess: () => { return Promise.all([this.firstTask(), this.secondTask()]) }, onFinish: (visit) => { // Not called until firstTask() and secondTask() have finished }, }) ``` \== React ```js import { router } from '@inertiajs/react' router.post(url, { onSuccess: () => { return Promise.all([this.firstTask(), this.secondTask()]) }, onFinish: (visit) => { // Not called until firstTask() and secondTask() have finished }, }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.post(url, { onSuccess: () => { return Promise.all([this.firstTask(), this.secondTask()]) }, onFinish: (visit) => { // Not called until firstTask() and secondTask() have finished }, }) ``` ::: --- --- url: /guide/merging-props.md --- # Merging Props Inertia overwrites props with the same name when reloading a page. However, you may need to merge new data with existing data instead. For example, when implementing a "load more" button for paginated results. The [Infinite scroll](/guide/infinite-scroll) component uses prop merging under the hood. Prop merging only works during [partial reloads](/guide/partial-reloads). Full page visits will always replace props entirely, even if you've marked them for merging. ## Merge Methods @available\_since rails=3.8.0 core=2.0.8 To merge a prop instead of overwriting it, you may use the `InertiaRails.merge` method when returning your response. ```ruby class UsersController < ApplicationController include Pagy::Method def index _pagy, records = pagy(:offset, User.all) render inertia: { users: InertiaRails.merge { records.as_json(...) }, } end end ``` The `InertiaRails.merge` method will append new items to existing arrays at the root level. ```ruby # Append at root level (default)... InertiaRails.merge { items } ``` @available\_since rails=3.12.0 core=2.2.0 You may change this behavior to prepend items instead. ```ruby # Prepend at root level... InertiaRails.merge(prepend: true) { items } ``` For more precise control, you can target specific nested properties for merging while replacing the rest of the object. ```ruby # Only append to the 'data' array, replace everything else... InertiaRails.merge(append: 'data') { {data: data, meta: meta} } # Prepend to the 'messages' array... InertiaRails.merge(prepend: 'messages') { chat_data } ``` You can combine multiple operations and target several properties at once. ```ruby InertiaRails.merge( append: 'posts', prepend: 'announcements' ) { forum_data } # Target multiple properties... InertiaRails.merge( append: ['notifications', 'activities'] ) { dashboard_data } ``` On the client side, Inertia handles all the merging automatically according to your server-side configuration. ## Matching Items @available\_since rails=3.8.0 core=2.0.8 When merging arrays, you may use the `match_on` parameter to match existing items by a specific field and update them instead of appending new ones. ```ruby # Match posts by ID, update existing ones... InertiaRails.merge(match_on: 'id') { post_data } ``` @available\_since rails=3.12.0 core=2.2.0 You may also use append and prepend with a hash to specify the field to match. ```ruby # Match posts by ID, update existing ones... InertiaRails.merge(append: 'data', match_on: 'data.id') { post_data } # Same as above, but using a hash shortcut... InertiaRails.merge(append: { data: 'id' }) { post_data } # Multiple properties with different match fields... InertiaRails.merge(append: { 'users.data' => 'id', 'messages' => 'uuid', }) { complex_data } ``` In the first two examples, Inertia will iterate over the data array and attempt to match each item by its id field. If a match is found, the existing item will be replaced. If no match is found, the new item will be appended. ## Deep Merge @available\_since rails=3.8.0 core=2.0.8 Instead of specifying which nested paths should be merged, you may use `InertiaRails.deep_merge` to ensure a deep merge of the entire structure. ```ruby class ChatController < ApplicationController def index chat_data = [ messages: [ [id: 4, text: 'Hello there!', user: 'Alice'], [id: 5, text: 'How are you?', user: 'Bob'], ], online: 12, ] render inertia: { chat: InertiaRails.deep_merge(chat_data, match_on: 'messages.id') } end end ``` > \[!NOTE] > `InertiaRails.deep_merge` was introduced before `InertiaRails.merge` had support for prepending and targeting nested paths. In most cases, `InertiaRails.merge` with its append and prepend parameters should be sufficient. ## Client Side Visits You can also merge props directly on the client side without making a server request using [client side visits](/guide/manual-visits#client-side-visits). Inertia provides [prop helper methods](/guide/manual-visits#prop-helpers) that allow you to append, prepend, or replace prop values. ## Combining with Deferred Props You may combine [deferred props](/guide/deferred-props) with mergeable props to defer the loading of the prop and ultimately mark it as mergeable once it's loaded. ```ruby class UsersController < ApplicationController include Pagy::Method def index pagy, records = pagy(:offset, User.all) render inertia: { results: InertiaRails.defer(deep_merge: true) { records.as_json(...) }, } end end ``` ## Combining with Once Props @available\_since rails=3.15.0 core=2.2.20 You may pass the `once: true` argument to a deferred prop to ensure the data is resolved only once and remembered by the client across subsequent navigations. ```ruby class UsersController < ApplicationController def index render inertia: { activity: InertiaRails.merge(once: true) { @user.recent_activity }, } end end ``` For more information on once props, see the [once props](/guide/once-props) documentation. ## Resetting Props On the client side, you can indicate to the server that you would like to reset the prop. This is useful when you want to clear the prop value before merging new data, such as when the user enters a new search query on a paginated list. The `reset` request option accepts an array of the props keys you would like to reset. ```js router.reload({ reset: ['results'], // ... }) ``` --- --- url: /guide/once-props.md --- # Once Props @available\_since rails=3.15.0 core=2.2.20 Some data rarely changes, is expensive to compute, or is simply large. Rather than including this data in every response, you may use *once props*. These props are remembered by the client and reused on subsequent pages that include the same prop. This makes them ideal for [shared data](/guide/shared-data). > \[!NOTE] > To understand when to use once props vs cached props vs HTTP caching, see the [Caching](/guide/caching) guide. ## Creating Once Props To create a once prop, use the `InertiaRails.once` method when returning your response. This method receives a block that returns the prop data. ```ruby class BillingController < ApplicationController def index render inertia: { plans: InertiaRails.once { Plan.all }, } end end ``` After the client has received this prop, subsequent requests will skip resolving the block and exclude the prop from the response. The client only remembers once props while navigating between pages that include them. Navigating to a page without the once prop will forget the remembered value, and it will be resolved again on the next page that has it. In practice, this is rarely an issue since once props are typically used as shared data or within a specific section of your application. ## Forcing a Refresh You may force a once prop to be refreshed using the `fresh` parameter. This is useful when you need to invalidate cached data based on particular pages, user actions or external changes: ```ruby class BillingController < ApplicationController def index render inertia: { plans: InertiaRails.once(fresh: params[:refresh_plans].present?) { Plan.all }, } end end ``` ## Refreshing from the Client You may refresh a once prop from the client-side using a [partial reload](/guide/partial-reloads). The server will always resolve a once prop when explicitly requested. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.reload({ only: ['plans'] }) ``` \== React ```js import { router } from '@inertiajs/react' router.reload({ only: ['plans'] }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.reload({ only: ['plans'] }) ``` ::: ## Expiration You may set an expiration time using the `expires_in` parameter. This parameter accepts an `ActiveSupport::Duration` or an integer (seconds). The prop will be refreshed on a subsequent visit after the expiration time has passed. ```ruby class DashboardController < ApplicationController def index render inertia: { plans: InertiaRails.once(expires_in: 1.day) { Plan.all }, } end end ``` ## Custom Keys You may assign a custom key to the prop using the `key` parameter. This is useful when you want to share data across multiple pages while using different prop names. ```ruby class TeamsController < ApplicationController def index render inertia: { memberRoles: InertiaRails.once(key: 'roles') { Role.all }, } end def invite render inertia: { availableRoles: InertiaRails.once(key: 'roles') { Role.all }, } end end ``` Both pages share the same underlying data because they use the same custom key, so the prop is only resolved for whichever page you visit first. ## Sharing Once Props You may share once props globally using the `inertia_share` controller method. ```ruby class ApplicationController < ActionController::Base inertia_share countries: InertiaRails.once { Country.all } end ``` You can also use `expires_in` and `key` options when sharing once props: ```ruby class ApplicationController < ActionController::Base # Refresh countries list daily inertia_share countries: InertiaRails.once(expires_in: 1.day) { Country.all } # Share roles under a custom key for cross-page sharing inertia_share roles: InertiaRails.once(key: 'user_roles') { Role.all } end ``` ## Prefetching Once props are compatible with [prefetching](/guide/prefetching). The client automatically includes any remembered once props in prefetched responses, so navigating to a prefetched page will already have the once props available. Prefetched pages containing an expired once prop will be invalidated from the cache. ## Combining with Other Prop Types The `once` option can be passed to [deferred](/guide/deferred-props), [merge](/guide/merging-props), and [optional](/guide/partial-reloads#lazy-data-evaluation) props. ```ruby class DashboardController < ApplicationController def index render inertia: { memberRoles: InertiaRails.once(key: 'roles') { Role.all }, permissions: InertiaRails.defer(once: true) { Permission.all }, activity: InertiaRails.merge(once: true) { @user.recent_activity }, categories: InertiaRails.optional(once: true) { Category.all }, } end end ``` --- --- url: /guide/optimistic-updates.md --- # Optimistic Updates Inertia allows you to update the UI immediately without waiting for the server to respond, such as incrementing a like counter, toggling a bookmark, or adding an item to a list. Optimistic updates apply changes instantly while the request is in flight, automatically rolling back if the request fails. ## Router Visits You may chain the `optimistic()` method before any router visit. The callback receives the current page props and should return a partial update to apply immediately. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router .optimistic((props) => ({ post: { ...props.post, likes: props.post.likes + 1, }, })) .post(`/posts/${post.id}/like`) ``` \== React ```js import { router } from '@inertiajs/react' router .optimistic((props) => ({ post: { ...props.post, likes: props.post.likes + 1, }, })) .post(`/posts/${post.id}/like`) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router .optimistic((props) => ({ post: { ...props.post, likes: props.post.likes + 1, }, })) .post(`/posts/${post.id}/like`) ``` ::: The optimistic update is applied immediately to the current page's props, so your component re-renders with the new values before the request is sent. When the server responds, Inertia replaces the optimistic data with the server's response. If the request fails, the props are automatically reverted to their original values. ## Form Component The `
    ` component supports optimistic updates via the `optimistic` prop. Since the component manages input data internally, the form data is provided as a second callback argument. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx ({ todos: [...props.todos, { id: Date.now(), name: data.name, done: false }], })} >
    ``` \== Svelte ```svelte
    ({ todos: [...props.todos, { id: Date.now(), name: data.name, done: false }], })} >
    ``` ::: ## Form Helper The `useForm` helper also supports optimistic updates via the same `optimistic()` method. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { useForm } from '@inertiajs/react' export default function Posts({ posts }) { const { data, setData, optimistic, post, processing } = useForm({ title: '', }) function save(e) { e.preventDefault() optimistic((props) => ({ posts: [...props.posts, { title: data.title }], })) post('/posts') } return (
    setData('title', e.target.value)} />
    ) } ``` \== Svelte ```svelte ``` ::: ## HTTP Requests The [`useHttp`](/guide/http-requests) hook supports optimistic updates as well. Since HTTP requests don't interact with Inertia's page props, the optimistic callback receives and updates the form's own data. On failure, the form data is reverted to its pre-request state. :::tabs key:frameworks \== Vue ```js import { useHttp } from '@inertiajs/vue3' const form = useHttp({ likes: 0, }) form .optimistic((data) => ({ likes: data.likes + 1, })) .post('/api/likes') ``` \== React ```js import { useHttp } from '@inertiajs/react' const { optimistic, post } = useHttp({ likes: 0, }) optimistic((data) => ({ likes: data.likes + 1, })) post('/api/likes') ``` \== Svelte ```js import { useHttp } from '@inertiajs/svelte' const form = useHttp({ likes: 0, }) form .optimistic((data) => ({ likes: data.likes + 1, })) .post('/api/likes') ``` ::: ## How It Works When an optimistic update is applied: 1. The returned props are compared against the current page props, and only the keys that actually changed are snapshotted 2. The callback's return value is merged into the current data 3. The request is sent to the server 4. On success, the server's response replaces the optimistic data 5. On failure, only the snapshotted keys are reverted, rolling back the optimistic changes The callback should return a **partial** object containing only the keys you wish to update. The returned values are shallow-merged with the current data. ### Automatic Rollback Optimistic state is automatically reverted in several scenarios: * **Validation errors (422)**: The optimistic state is reverted and the validation errors are preserved * **Server errors**: When the request fails for any other reason, the original state is restored * **Interrupted visits**: When a new visit interrupts an in-flight request, the previous optimistic state is restored before the new optimistic update is applied ### Concurrent Updates Multiple optimistic requests may be in flight at the same time. Inertia tracks which props each optimistic update touched, and server responses will not overwrite a prop until the last optimistic request that modified it has resolved. ## Inline Option You may also pass the optimistic callback directly in the visit options instead of chaining. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.post( `/posts/${post.id}/like`, {}, { optimistic: (props) => ({ post: { ...props.post, likes: props.post.likes + 1 }, }), }, ) ``` \== React ```js import { router } from '@inertiajs/react' router.post( `/posts/${post.id}/like`, {}, { optimistic: (props) => ({ post: { ...props.post, likes: props.post.likes + 1 }, }), }, ) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.post( `/posts/${post.id}/like`, {}, { optimistic: (props) => ({ post: { ...props.post, likes: props.post.likes + 1 }, }), }, ) ``` ::: The inline option works with `useHttp` submit methods as well. ```js form.post('/api/likes', { optimistic: (data) => ({ likes: data.likes + 1, }), }) ``` --- --- url: /guide/pages.md --- # Pages When building applications using Inertia, each page in your application typically has its own controller / route and a corresponding JavaScript component. This allows you to retrieve just the data necessary for that page - no API required. In addition, all of the data needed for the page can be retrieved before the page is ever rendered by the browser, eliminating the need for displaying "loading" states when users visit your application. ## Creating Pages Inertia pages are simply JavaScript components. If you have ever written a Vue, React, or Svelte component, you will feel right at home. As you can see in the example below, pages receive data from your application's controllers as props. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import Layout from '../Layout' import { Head } from '@inertiajs/react' export default function Welcome({ user }) { return (

    Welcome

    Hello {user.name}, welcome to your first Inertia app!

    ) } ``` \== Svelte ```svelte Welcome

    Welcome

    Hello {user.name}, welcome to your first Inertia app!

    ``` ::: Given the page above, you can render the page by returning an [Inertia response](/guide/responses) from a controller or route. In this example, let's assume this page is stored at `app/frontend/pages/users/show.vue` `app/frontend/pages/users/show.jsx` `app/frontend/pages/users/show.svelte` within a Rails application. ```ruby class UsersController < ApplicationController def show user = User.find(params[:id]) render inertia: { user: } end end ``` If you attempt to render a page that does not exist, the response will typically be a blank screen. ## Layouts Most applications share common UI elements across pages. Inertia provides persistent layouts that survive page navigations, along with layout props for passing dynamic data between pages and their layouts. Visit the [layouts documentation](/guide/layouts) to learn more. --- --- url: /guide/partial-reloads.md --- # Partial Reloads When making visits to the same page you are already on, it's not always necessary to re-fetch all of the page's data from the server. In fact, selecting only a subset of the data can be a helpful performance optimization if it's acceptable that some page data becomes stale. Inertia makes this possible via its "partial reload" feature. As an example, consider a "user index" page that includes a list of users, as well as an option to filter the users by their company. On the first request to the page, both the `users` and `companies` props are passed to the page component. However, on subsequent visits to the same page (maybe to filter the users), you can request only the `users` data from the server without requesting the `companies` data. Inertia will then automatically merge the partial data returned from the server with the data it already has in memory client-side. > \[!NOTE] > Partial reloads only work for visits made to the same page component. ## Only Certain Props To perform a partial reload, use the `only` visit option to specify which data the server should return. This option should be an array of keys which correspond to the keys of the props. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.visit(url, { only: ['users'], }) ``` \== React ```js import { router } from '@inertiajs/react' router.visit(url, { only: ['users'], }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.visit(url, { only: ['users'], }) ``` ::: ## Except Certain Props In addition to the `only` visit option you can also use the `except` option to specify which data the server should exclude. This option should also be an array of keys which correspond to the keys of the props. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.visit(url, { except: ['users'], }) ``` \== React ```js import { router } from '@inertiajs/react' router.visit(url, { except: ['users'], }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.visit(url, { except: ['users'], }) ``` ::: ## Router Shorthand Since partial reloads can only be made to the same page component the user is already on, it almost always makes sense to just use the `router.reload()` method, which automatically uses the current URL. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.reload({ only: ['users'] }) ``` \== React ```js import { router } from '@inertiajs/react' router.reload({ only: ['users'] }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.reload({ only: ['users'] }) ``` ::: ## Using Links It's also possible to perform partial reloads with Inertia links using the `only` property. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( Show active ) ``` \== Svelte ```svelte Show active Show active ``` ::: ## Lazy Data Evaluation For partial reloads to be most effective, be sure to also use lazy data evaluation when returning props from your server-side routes or controllers. This can be accomplished by wrapping all optional page data in a lambda. ```ruby class UsersController < ApplicationController def index render inertia: { users: -> { User.all }, companies: -> { Company.all }, } end end ``` When Inertia performs a request, it will determine which data is required and only then will it evaluate the lambda. This can significantly increase the performance of pages that contain a lot of optional data. Additionally, Inertia provides an `InertiaRails.optional` method to specify that a prop should never be included unless explicitly requested using the `only` option: ```ruby class UsersController < ApplicationController def index render inertia: { users: InertiaRails.optional { User.all }, } end end ``` > \[!NOTE] > Prior to Inertia.js v2, the method `InertiaRails.lazy` was used. It is now deprecated and has been replaced by `InertiaRails.optional`. Please update your code accordingly to ensure compatibility with the latest version. On the inverse, you can use the `InertiaRails.always` method to specify that a prop should always be included, even if it has not been explicitly required in a partial reload. ```ruby class UsersController < ApplicationController def index render inertia: { users: InertiaRails.always { User.all }, } end end ``` Here's a summary of each approach: | Approach | Standard Visits | Partial Reloads | Evaluated | | :---------------------------------------------------------------------------- | :-------------- | :-------------- | :--------------- | | `User.all` | Always | Optionally | Always | | `-> { User.all }` | Always | Optionally | Only when needed | | `InertiaRails.optional { User.all }` | Never | Optionally | Only when needed | | `InertiaRails.always { User.all }` | Always | Always | Always | ## Preserving Errors @available\_since core=3.0.0 Since the Rails adapter shares errors using `InertiaRails.always`, they are included in every response, even partial reloads where no validation runs. This means an empty errors hash from the server will overwrite any existing client-side validation errors. You may use the `preserveErrors` option to keep existing errors when the server doesn't return new ones. :::tabs key:frameworks \== Vue ```js router.reload({ only: ['posts'], preserveErrors: true, }) ``` \== React ```js router.reload({ only: ['posts'], preserveErrors: true, }) ``` \== Svelte ```js router.reload({ only: ['posts'], preserveErrors: true, }) ``` ::: ## Combining with Once Props @available\_since rails=3.15.0 core=2.2.20 You may pass the `once: true` argument to a deferred prop to ensure the data is resolved only once and remembered by the client across subsequent navigations. ```ruby class UsersController < ApplicationController def index render inertia: { users: InertiaRails.optional(once: true) { User.all }, } end end ``` For more information on once props, see the [once props](/guide/once-props) documentation. ## Combining with Caching @available\_since rails=3.21.0 You may pass the `cache` option to an optional prop to cache the resolved value on the server side. On cache hits, the block is not evaluated. ```ruby class UsersController < ApplicationController def index render inertia: { users: InertiaRails.optional(cache: 'all_users') { User.all }, } end end ``` For more information on cache keys and options, see the [cached props](/guide/cached-props) documentation. --- --- url: /guide/polling.md --- # Polling ## Poll Helper Polling your server for new information on the current page is common, so Inertia provides a poll helper designed to help reduce the amount of boilerplate code. In addition, the poll helper will automatically stop polling when the page is unmounted. The only required argument is the polling interval in milliseconds. :::tabs key:frameworks \== Vue ```js import { usePoll } from '@inertiajs/vue3' usePoll(2000) ``` \== React ```jsx import { usePoll } from '@inertiajs/react' usePoll(2000) ``` \== Svelte ```js import { usePoll } from '@inertiajs/svelte' usePoll(2000) ``` ::: If you need to pass additional request options to the poll helper, you can pass any of the `router.reload` options as the second parameter. :::tabs key:frameworks \== Vue ```js import { usePoll } from '@inertiajs/vue3' usePoll(2000, { onStart() { console.log('Polling request started') }, onFinish() { console.log('Polling request finished') }, }) ``` \== React ```jsx import { usePoll } from '@inertiajs/react' usePoll(2000, { onStart() { console.log('Polling request started') }, onFinish() { console.log('Polling request finished') }, }) ``` \== Svelte ```js import { usePoll } from '@inertiajs/svelte' usePoll(2000, { onStart() { console.log('Polling request started') }, onFinish() { console.log('Polling request finished') }, }) ``` ::: If you'd like more control over the polling behavior, the poll helper provides `stop` and `start` methods that allow you to manually start and stop polling. You can pass the `autoStart: false` option to the poll helper to prevent it from automatically starting polling when the component is mounted. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { usePoll } from '@inertiajs/react' export default () => { const { start, stop } = usePoll( 2000, {}, { autoStart: false, }, ) return (
    ) } ``` \== Svelte ```svelte ``` ::: ## Throttling By default, the poll helper will throttle requests by 90% when the browser tab is in the background. If you'd like to disable this behavior, you can pass the `keepAlive` option to the poll helper. :::tabs key:frameworks \== Vue ```js import { usePoll } from '@inertiajs/vue3' usePoll( 2000, {}, { keepAlive: true, }, ) ``` \== React ```jsx import { usePoll } from '@inertiajs/react' usePoll( 2000, {}, { keepAlive: true, }, ) ``` \== Svelte ```js import { usePoll } from '@inertiajs/svelte' usePoll( 2000, {}, { keepAlive: true, }, ) ``` ::: --- --- url: /guide/prefetching.md --- # Prefetching Inertia supports prefetching data for pages that are likely to be visited next. This can be useful for improving the perceived performance of your app by allowing the data to be fetched in the background while the user is still interacting with the current page. ## Link Prefetching To prefetch data for a page, you can add the `prefetch` prop to the Inertia link component. By default, Inertia will prefetch the data for the page when the user hovers over the link for more than 75ms. You may customize this hover delay by setting the `prefetch.hoverDelay` option in your [application defaults](/guide/client-side-setup#configuring-defaults). :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( Users ) ``` \== Svelte ```svelte Users ``` ::: By default, data is cached for 30 seconds before being evicted. You may customize this default value by setting the `prefetch.cacheFor` option in your [application defaults](/guide/client-side-setup#configuring-defaults). You may also customize the cache duration on a per-link basis by passing a `cacheFor` prop to the `Link` component. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( <> Users Users Users ) ``` \== Svelte ```svelte Users Users Users ``` ::: Instead of prefetching on hover, you can also start prefetching on `mousedown` by passing the `click` value to the `prefetch` prop. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( Users ) ``` \== Svelte ```svelte Users ``` ::: If you're confident that the user will visit a page next, you can prefetch the data on mount as well. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( Users ) ``` \== Svelte ```svelte Users ``` ::: You can also combine prefetch strategies by passing an array of values to the `prefetch` prop. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( Users ) ``` \== Svelte ```svelte Users ``` ::: ## Programmatic Prefetching You can prefetch data programmatically using `router.prefetch`. This method's signature is identical to `router.visit` with the exception of a third argument that allows you to specify prefetch options. When the `cacheFor` option is not specified, it defaults to 30 seconds. ```js router.prefetch('/users', { method: 'get', data: { page: 2 } }) router.prefetch( '/users', { method: 'get', data: { page: 2 } }, { cacheFor: '1m' }, ) ``` Inertia also provides a `usePrefetch` hook that allows you to track the prefetch state for the current page. It returns information about whether the page is currently prefetching, has been prefetched, when it was last updated, and a `flush` method that flushes the cache for the current page only. :::tabs key:frameworks \== Vue ```js import { usePrefetch } from '@inertiajs/vue3' const { lastUpdatedAt, isPrefetching, isPrefetched, flush } = usePrefetch() ``` \== React ```js import { usePrefetch } from '@inertiajs/react' const { lastUpdatedAt, isPrefetching, isPrefetched, flush } = usePrefetch() ``` \== Svelte ```js import { usePrefetch } from '@inertiajs/svelte' const { lastUpdatedAt, isPrefetching, isPrefetched, flush } = usePrefetch() ``` ::: You can also pass visit options when you need to differentiate between different request configurations for the same URL. :::tabs key:frameworks \== Vue ```js import { usePrefetch } from '@inertiajs/vue3' const { lastUpdatedAt, isPrefetching, isPrefetched, flush } = usePrefetch({ headers: { 'X-Custom-Header': 'value' }, }) ``` \== React ```js import { usePrefetch } from '@inertiajs/react' const { lastUpdatedAt, isPrefetching, isPrefetched, flush } = usePrefetch({ headers: { 'X-Custom-Header': 'value' }, }) ``` \== Svelte ```js import { usePrefetch } from '@inertiajs/svelte' const { lastUpdatedAt, isPrefetching, isPrefetched, flush } = usePrefetch({ headers: { 'X-Custom-Header': 'value' }, }) ``` ::: ## Cache Tags @available\_since core=2.1.2 Cache tags allow you to group related prefetched data and invalidate all cached data with that tag when specific events occur. To tag cached data, pass a `cacheTags` prop to your `Link` component. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( <> Users Dashboard ) ``` \== Svelte ```svelte import {inertia} from '@inertiajs/svelte' Users Dashboard ``` ::: When prefetching programmatically, pass `cacheTags` in the third argument to `router.prefetch`. ```js router.prefetch('/users', {}, { cacheTags: 'users' }) router.prefetch('/dashboard', {}, { cacheTags: ['dashboard', 'stats'] }) ``` ## Cache Invalidation You can manually flush the prefetch cache by calling `router.flushAll` to remove all cached data, or `router.flush` to remove cache for a specific page. ```js // Flush all prefetch cache router.flushAll() // Flush cache for a specific page router.flush('/users', { method: 'get', data: { page: 2 } }) // Using the usePrefetch hook const { flush } = usePrefetch() // Flush cache for the current page flush() ``` For more granular control, you can flush cached data by their tags using `router.flushByCacheTags`. This removes any cached response that contains *any* of the specified tags. ```js // Flush all responses tagged with 'users' router.flushByCacheTags('users') // Flush all responses tagged with 'dashboard' OR 'stats' router.flushByCacheTags(['dashboard', 'stats']) ``` ### Automatic Cache Flushing By default, Inertia does not automatically flush the prefetch cache when you navigate to new pages. Cached data is only evicted when it expires based on the cache duration. If you want to flush all cached data on every navigation, you can set up an event listener. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.on('navigate', () => router.flushAll()) ``` \== React ```js import { router } from '@inertiajs/react' router.on('navigate', () => router.flushAll()) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.on('navigate', () => router.flushAll()) ``` ::: ### Invalidate on Requests @available\_since core=2.1.2 To automatically invalidate caches when making requests, pass an `invalidateCacheTags` prop to the `Form` component. The specified tags will be flushed when the form submission succeeds. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Form } from '@inertiajs/react' export default () => (
    ) ``` \== Svelte ```svelte
    ``` ::: When using the `useForm` helper, you can include `invalidateCacheTags` in the visit options. :::tabs key:frameworks \== Vue ```js import { useForm } from '@inertiajs/vue3' const form = useForm({ name: '', email: '', }) const submit = () => { form.post('/users', { invalidateCacheTags: ['users', 'dashboard'], }) } ``` \== React ```js import { useForm } from '@inertiajs/react' const { data, setData, post } = useForm({ name: '', email: '', }) function submit(e) { e.preventDefault() post('/users', { invalidateCacheTags: ['users', 'dashboard'], }) } ``` \== Svelte ```js import { useForm } from '@inertiajs/svelte' const form = useForm({ name: '', email: '', }) function submit() { form.post('/users', { invalidateCacheTags: ['users', 'dashboard'], }) } ``` ::: You can also invalidate cache tags with programmatic visits by including `invalidateCacheTags` in the options. ```js router.delete(`/users/${userId}`, { invalidateCacheTags: ['users', 'dashboard'], }) router.post('/posts', postData, { invalidateCacheTags: ['posts', 'recent-posts'], }) ``` ## Stale While Revalidate By default, Inertia will fetch a fresh copy of the data when the user visits the page if the cached data is older than the cache duration. You can customize this behavior by passing a tuple to the `cacheFor` prop. The first value in the array represents the number of seconds the cache is considered fresh, while the second value defines how long it can be served as stale data before fetching data from the server is necessary. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( Users ) ``` \== Svelte ```svelte Users ``` ::: If a request is made within the fresh period (before the first value), the cache is returned immediately without making a request to the server. If a request is made during the stale period (between the two values), the stale value is served to the user, and a request is made in the background to refresh the cached data. Once the fresh data is returned, it is merged into the page so the user has the most recent data. If a request is made after the second value, the cache is considered expired, and the page and data is fetched from the sever as a regular request. --- --- url: /guide/progress-indicators.md --- # Progress Indicators Since Inertia requests are made via XHR, there would typically not be a browser loading indicator when navigating from one page to another. To solve this, Inertia displays a progress indicator at the top of the page whenever you make an Inertia visit. However, [asynchronous requests](#visit-options) do not show the progress indicator unless explicitly configured. Of course, if you prefer, you can disable Inertia's default loading indicator and provide your own custom implementation. We'll discuss both approaches below. ## Default Inertia's default progress indicator is a light-weight wrapper around the [NProgress](https://ricostacruz.com/nprogress/) library. You can customize it via the `progress` property of the `createInertiaApp()` function. ```js createInertiaApp({ progress: { // The delay after which the progress bar will appear, in milliseconds... delay: 250, // The color of the progress bar... color: '#29d', // Whether to include the default NProgress styles... includeCSS: true, // Whether the NProgress spinner will be shown... showSpinner: false, }, // ... }) ``` You can disable Inertia's default loading indicator by setting the `progress` property to `false`. ```js createInertiaApp({ progress: false, // ... }) ``` ## Programmatic Access @available\_since core=2.1.11 When you need to control the progress indicator outside of Inertia requests, for example, when making requests with Axios or other libraries, you can use Inertia's progress methods directly. :::tabs key:frameworks \== Vue ```js import { progress } from '@inertiajs/vue3' progress.start() // Begin progress animation progress.set(0.25) // Set to 25% complete progress.finish() // Complete and fade out progress.reset() // Reset to start progress.remove() // Complete and remove from DOM progress.hide() // Hide progress bar progress.reveal() // Show progress bar progress.isStarted() // Returns boolean progress.getStatus() // Returns current percentage or null ``` \== React ```js import { progress } from '@inertiajs/react' progress.start() // Begin progress animation progress.set(0.25) // Set to 25% complete progress.finish() // Complete and fade out progress.reset() // Reset to start progress.remove() // Complete and remove from DOM progress.hide() // Hide progress bar progress.reveal() // Show progress bar progress.isStarted() // Returns boolean progress.getStatus() // Returns current percentage or null ``` \== Svelte ```js import { progress } from '@inertiajs/svelte' progress.start() // Begin progress animation progress.set(0.25) // Set to 25% complete progress.finish() // Complete and fade out progress.reset() // Reset to start progress.remove() // Complete and remove from DOM progress.hide() // Hide progress bar progress.reveal() // Show progress bar progress.isStarted() // Returns boolean progress.getStatus() // Returns current percentage or null ``` ::: The `hide()` and `reveal()` methods work together to prevent conflicts when separate parts of your code need to control progress visibility. Each `hide()` call increments an internal counter, while `reveal()` decrements it. The progress bar only appears when this counter reaches zero. However, `reveal()` accepts an optional `force` parameter that bypasses this counter. Inertia uses this pattern internally to hide progress during prefetching while ensuring it appears for actual navigation. ```js progress.hide() // Counter = 1, bar hidden progress.hide() // Counter = 2, bar still hidden progress.reveal() // Counter = 1, bar still hidden progress.reveal() // Counter = 0, bar now visible // Force reveal bypasses the counter progress.reveal(true) ``` > \[!NOTE] > If you've disabled the progress indicator with `progress: false` in `createInertiaApp()`, these programmatic methods will not work. ## Custom It's also possible to setup your own custom page loading indicators using Inertia [events](/guide/events). Let's explore how to do this using the [NProgress](https://ricostacruz.com/nprogress/) library as an example. First, disable Inertia's default loading indicator. ```js createInertiaApp({ progress: false, // ... }) ``` Next, install the NProgress library. ```bash npm install nprogress ``` After installation, you'll need to add the NProgress [styles](https://github.com/rstacruz/nprogress/blob/master/nprogress.css) to your project. You can do this using a CDN hosted copy of the styles. ```html ``` Then, import both `NProgress` and the Inertia `router` into your application. :::tabs key:frameworks \== Vue ```js import NProgress from 'nprogress' import { router } from '@inertiajs/vue3' ``` \== React ```js import NProgress from 'nprogress' import { router } from '@inertiajs/react' ``` \== Svelte ```js import NProgress from 'nprogress' import { router } from '@inertiajs/svelte' ``` ::: Next, add a `start` event listener. We'll use this listener to show the progress bar when a new Inertia visit begins. ```js router.on('start', () => NProgress.start()) ``` Finally, add a `finish` event listener to hide the progress bar when the page visit finishes. ```js router.on('finish', () => NProgress.done()) ``` That's it! Now, as you navigate from one page to another, the progress bar will be added and removed from the page. ### Handling Cancelled Visits While this custom progress implementation works great for page visits that finish properly, it would be nice to handle cancelled visits as well. First, for interrupted visits (those that get cancelled as a result of a new visit), the progress bar should simply be reset back to the start position. Second, for manually cancelled visits, the progress bar should be immediately removed from the page. We can accomplish this by inspecting the `event.detail.visit` object that's provided to the finish event. ```js router.on('finish', (event) => { if (event.detail.visit.completed) { NProgress.done() } else if (event.detail.visit.interrupted) { NProgress.set(0) } else if (event.detail.visit.cancelled) { NProgress.done() NProgress.remove() } }) ``` ### File Upload Progress Let's take this a step further. When files are being uploaded, it would be great to update the loading indicator to reflect the upload progress. This can be done using the `progress` event. ```js router.on('progress', (event) => { if (event.detail.progress.percentage) { NProgress.set((event.detail.progress.percentage / 100) * 0.9) } }) ``` Now, instead of the progress bar "trickling" while the files are being uploaded, it will actually update it's position based on the progress of the request. We limit the progress here to 90%, since we still need to wait for a response from the server. ### Loading Indicator Delay The last thing we're going to implement is a loading indicator delay. It's often preferable to delay showing the loading indicator until a request has taken longer than 250-500 milliseconds. This prevents the loading indicator from appearing constantly on quick page visits, which can be visually distracting. To implement the delay behavior, we'll use the `setTimeout` and `clearTimeout` functions. Let's start by defining a variable to keep track of the timeout. ```js let timeout = null ``` Next, let's update the `start` event listener to start a new timeout that will show the progress bar after 250 milliseconds. ```js router.on('start', () => { timeout = setTimeout(() => NProgress.start(), 250) }) ``` Next, we'll update the `finish` event listener to clear any existing timeouts in the event that the page visit finishes before the timeout does. ```js router.on('finish', (event) => { clearTimeout(timeout) // ... }) ``` In the `finish` event listener, we need to determine if the progress bar has actually started displaying progress, otherwise we'll inadvertently cause it to show before the timeout has finished. ```js router.on('finish', (event) => { clearTimeout(timeout) if (!NProgress.isStarted()) { return } // ... }) ``` And, finally, we need to do the same check in the `progress` event listener. ```js router.on('progress', (event) => { if (!NProgress.isStarted()) { return } // ... }) ``` That's it, you now have a beautiful custom page loading indicator! ### Complete Example For convenience, here is the full source code of the final version of our custom loading indicator. :::tabs key:frameworks \== Vue ```js import NProgress from 'nprogress' import { router } from '@inertiajs/vue3' let timeout = null router.on('start', () => { timeout = setTimeout(() => NProgress.start(), 250) }) router.on('progress', (event) => { if (NProgress.isStarted() && event.detail.progress.percentage) { NProgress.set((event.detail.progress.percentage / 100) * 0.9) } }) router.on('finish', (event) => { clearTimeout(timeout) if (!NProgress.isStarted()) { return } if (event.detail.visit.completed) { NProgress.done() } else if (event.detail.visit.interrupted) { NProgress.set(0) } else if (event.detail.visit.cancelled) { NProgress.done() NProgress.remove() } }) ``` \== React ```js import NProgress from 'nprogress' import { router } from '@inertiajs/react' let timeout = null router.on('start', () => { timeout = setTimeout(() => NProgress.start(), 250) }) router.on('progress', (event) => { if (NProgress.isStarted() && event.detail.progress.percentage) { NProgress.set((event.detail.progress.percentage / 100) * 0.9) } }) router.on('finish', (event) => { clearTimeout(timeout) if (!NProgress.isStarted()) { return } if (event.detail.visit.completed) { NProgress.done() } else if (event.detail.visit.interrupted) { NProgress.set(0) } else if (event.detail.visit.cancelled) { NProgress.done() NProgress.remove() } }) ``` \== Svelte ```js import NProgress from 'nprogress' import { router } from '@inertiajs/svelte' let timeout = null router.on('start', () => { timeout = setTimeout(() => NProgress.start(), 250) }) router.on('progress', (event) => { if (NProgress.isStarted() && event.detail.progress.percentage) { NProgress.set((event.detail.progress.percentage / 100) * 0.9) } }) router.on('finish', (event) => { clearTimeout(timeout) if (!NProgress.isStarted()) { return } if (event.detail.visit.completed) { NProgress.done() } else if (event.detail.visit.interrupted) { NProgress.set(0) } else if (event.detail.visit.cancelled) { NProgress.done() NProgress.remove() } }) ``` ::: ## Visit Options In addition to these configurations, Inertia.js provides two visit options to control the loading indicator on a per-request basis: `showProgress` and `async`. These options offer greater control over how Inertia.js handles asynchronous requests and manages progress indicators. ### Showprogress The `showProgress` option provides fine-grained control over the visibility of the loading indicator during requests. ```js router.get('/settings', {}, { showProgress: false }) ``` ### Async The `async` option allows you to perform asynchronous requests without displaying the default progress indicator. It can be used in combination with the `showProgress` option. ```js // Disable the progress indicator router.get('/settings', {}, { async: true }) // Enable the progress indicator with async requests router.get('/settings', {}, { async: true, showProgress: true }) ``` --- --- url: /guide/redirects.md --- # Redirects When making a non-GET Inertia request manually or via a `` element, you should ensure that you always respond with a proper Inertia redirect response. For example, if your controller is creating a new user, your "create" endpoint should return a redirect back to a standard `GET` endpoint, such as your user "index" page. Inertia will automatically follow this redirect and update the page accordingly. ```ruby class UsersController < ApplicationController def create user = User.new(user_params) if user.save redirect_to users_url else redirect_to new_user_url, inertia: { errors: user.errors } end end private def user_params params.require(:user).permit(:name, :email) end end ``` ## 303 Response Code When redirecting after a `PUT`, `PATCH`, or `DELETE` request, you must use a `303` response code, otherwise the subsequent request will not be treated as a `GET` request. A `303` redirect is very similar to a `302` redirect; however, the follow-up request is explicitly changed to a `GET` request. If you're using one of our official server-side adapters, all redirects will automatically be converted to `303` redirects. ## Preserving Fragments @available\_since rails=3.19.0 core=3.0.0 Sometimes a user may visit a URL with a fragment, such as `/article/old-slug#section`, and the server needs to redirect to a different URL. The fragment from the original request is normally lost during the redirect. :::tabs key:frameworks \== Vue ```vue View section ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => View section ``` \== Svelte ```svelte View section ``` ::: You may preserve the fragment by passing `preserve_fragment` on the redirect response. The client will carry over the `#section` fragment to the redirect target, resulting in `/article/new-slug#section`. ```ruby redirect_to article_path(article), inertia: { preserve_fragment: true } ``` ## External Redirects Sometimes it's necessary to redirect to an external website, or even another non-Inertia endpoint in your app while handling an Inertia request. This can be accomplished using a server-side initiated `window.location` visit via the `inertia_location` method. ```ruby inertia_location index_path ``` The `inertia_location` method will generate a `409 Conflict` response and include the destination URL in the `X-Inertia-Location` header. When this response is received client-side, Inertia will automatically perform a `window.location = url` visit. --- --- url: /guide/remembering-state.md --- # Remembering State When navigating browser history, Inertia restores pages using prop data cached in history state. However, Inertia does not restore local page component state since this is beyond its reach. This can lead to outdated pages in your browser history. For example, if a user partially completes a form, then navigates away, and then returns back, the form will be reset and their work will be lost. To mitigate this issue, you can tell Inertia which local component state to save in the browser history. ## Saving Local State To save local component state to the history state, use the `useRemember` feature to tell Inertia which data it should remember. :::tabs key:frameworks \== Vue ```js import { useRemember } from '@inertiajs/vue3' const form = useRemember({ first_name: null, last_name: null, }) ``` \== React ```jsx import { useRemember } from '@inertiajs/react' export default function Profile() { const [formState, setFormState] = useRemember({ first_name: null, last_name: null, // ... }) // ... } ``` \== Svelte ```js import { useRemember } from '@inertiajs/svelte' const form = useRemember({ first_name: null, last_name: null, }) // ... ``` ::: Now, whenever your local `form` state changes, Inertia will automatically save this data to the history state and will also restore it on history navigation. ## Multiple Components If your page contains multiple components that use the remember functionality provided by Inertia, you need to provide a unique key for each component so that Inertia knows which data to restore to each component. :::tabs key:frameworks \== Vue ```js import { useRemember } from '@inertiajs/vue3' const form = useRemember( { first_name: null, last_name: null, }, 'Users/Create', ) ``` \== React ```jsx import { useRemember } from '@inertiajs/react' export default function Profile() { const [formState, setFormState] = useRemember( { first_name: null, last_name: null, }, 'Users/Create', ) } ``` \== Svelte ```js import { page, useRemember } from '@inertiajs/svelte' const form = useRemember( { first_name: null, last_name: null, }, 'Users/Create', ) ``` ::: If you have multiple instances of the same component on the page using the remember functionality, be sure to also include a unique key for each component instance, such as a model identifier. :::tabs key:frameworks \== Vue ```js import { useRemember } from '@inertiajs/vue3' const props = defineProps({ user: Object }) const form = useRemember( { first_name: null, last_name: null, }, `Users/Edit:${props.user.id}`, ) ``` \== React ```jsx import { useRemember } from '@inertiajs/react' export default function Profile() { const [formState, setFormState] = useRemember( { first_name: props.user.first_name, last_name: props.user.last_name, }, `Users/Edit:${this.user.id}`, ) } ``` \== Svelte ```js import { page, useRemember } from '@inertiajs/svelte' const form = useRemember( { first_name: page.props.user.first_name, last_name: page.props.user.last_name, }, `Users/Edit:${page.props.user.id}`, ) ``` ::: ## Form Helper If you're using the [Inertia form helper](/guide/forms#form-helper), you can pass a unique form key as the first argument when instantiating your form. This will cause the form data and errors to automatically be remembered. :::tabs key:frameworks \== Vue ```js import { useForm } from '@inertiajs/vue3' const form = useForm('CreateUser', data) const form = useForm(`EditUser:${props.user.id}`, data) ``` \== React ```js import { useForm } from '@inertiajs/react' const form = useForm('CreateUser', data) const form = useForm(`EditUser:${user.id}`, data) ``` \== Svelte ```js import { useForm } from '@inertiajs/svelte' const form = useForm('CreateUser', data) const form = useForm(`EditUser:${user.id}`, data) ``` ::: You may [exclude specific fields](/guide/forms#excluding-fields) from being remembered using the `dontRemember()` method. This is useful for sensitive fields like passwords that should not be stored in history state. ## Manually Saving State The `useRemember` hook watches for data changes and automatically saves those changes to the history state. Then, Inertia will restore the data on page load. However, it's also possible to manage this manually using the underlying `remember()` and `restore()` methods exposed by Inertia. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' // Save local component state to history state router.remember(data, 'my-key') // Restore local component state from history state let data = router.restore('my-key') ``` \== React ```js import { router } from '@inertiajs/react' // Save local component state to history state router.remember(data, 'my-key') // Restore local component state from history state let data = router.restore('my-key') ``` \== Svelte ```js import { router } from '@inertiajs/svelte' // Save local component state to history state router.remember(data, 'my-key') // Restore local component state from history state let data = router.restore('my-key') ``` ::: > \[!WARNING] > Some browsers limit the number of `history.replaceState()` calls allowed > within a short time period. Inertia catches this error and logs it to the > console, but the state update will be lost. Avoid calling `router.remember()` > too frequently, and consider debouncing or batching state updates in > high-frequency scenarios. --- --- url: /guide/responses.md --- # Responses ## Creating Responses Creating an Inertia response is simple. By default, Inertia Rails follows convention over configuration: you pass the props (data) you wish to send to the page, and the component name is automatically inferred from the controller and action. In the example below, we will pass a single prop (`user`) to the `users/show` page component. ```ruby class UsersController < ApplicationController def show user = User.find(params[:id]) render inertia: { user: } # Renders '../users/show.jsx|vue|svelte' end end ``` Within Rails applications, the `UsersController#show` action would typically correspond to the file located at `app/frontend/pages/users/show.(jsx|vue|svelte)`. > \[!WARNING] > To ensure that pages load quickly, only return the minimum data required for the page. Also, be aware that **all data returned from the controllers will be visible client-side**, so be sure to omit sensitive information. ### Customizing the Component Path While the default convention works for most cases, you may need to render a specific component or change how component paths are resolved globally. #### Explicit Component Names If you wish to render a component that does not match the current controller action, you can explicitly provide the name of the [JavaScript page component](/guide/pages) followed by the props hash. ```ruby class EventsController < ApplicationController def my_event event = Event.find(params[:id]) render inertia: 'events/show', props: { event: event.as_json( only: [:id, :title, :start_date, :description] ) } end end ``` #### Custom Path Resolver If the default automatic path resolution does not match your project's conventions, you can define a custom resolution method via the `component_path_resolver` config value. The value should be callable and will receive the `path` and `action` parameters, returning a string component path. ```ruby inertia_config( component_path_resolver: ->(path:, action:) do "storefront/#{path.camelize}/#{action.camelize}" end ) ``` ### Using Instance Variables as Props For convenience, Inertia can automatically pass your controller's instance variables to the page component as props. To enable this behavior, invoke the `use_inertia_instance_props` method within your controller or a base controller. ```ruby class EventsController < ApplicationController use_inertia_instance_props def index @events = Event.all render inertia: 'events/index' end end ``` In this example, the `@events` instance variable is automatically included in the response as the `events` prop. Please note that if you manually provide a props hash in your render call, the instance variables feature is disabled for that specific response. > \[!WARNING] > Security and Performance Risk > > When enabled, this feature serializes all instance variables present in the controller at the time of rendering. This includes: > > * Variables set by `before_action` filters (e.g., `@current_user`, `@breadcrumbs`) called **after** `use_inertia_instance_props`. > * Memoized variables (often used for caching internal state, e.g., `@_cached_result`). > * Variables intended only for server-side logic. > > This creates a high risk of accidentally leaking sensitive data or internal implementation details to the client. It can also negatively impact performance by serializing unnecessary heavy objects. We recommend being explicit with your props whenever possible. ## Root Template Data There are situations where you may want to access your prop data in your ERB template. For example, you may want to add a meta description tag, Twitter card meta tags, or Facebook Open Graph meta tags. Inertia Rails provides several view helpers for working with Inertia data in your templates: ### `inertia_page` Returns the [page object](/guide/the-protocol#the-page-object) hash containing `component`, `props`, `url`, and other Inertia data. Use this to access page data in your root template. ```erb # app/views/inertia.html.erb <% content_for(:head) do %> <% end %> <%= inertia_root %> ``` ### `inertia_rendering?` Returns `true` when the current request is rendering an Inertia response. This is useful for conditionally rendering content in shared layouts. ```erb # app/views/layouts/application.html.erb <% if inertia_rendering? %> <%= yield %> <% else %>
    <%= yield %>
    <% end %> ``` ### `inertia_root` Renders the root element for the Inertia app. This helper automatically respects the [`root_dom_id`](/guide/configuration#root_dom_id) and [`use_script_element_for_initial_page`](/guide/configuration#use_script_element_for_initial_page) configuration options. ```erb <%= inertia_root %> ``` You can also pass a custom `id` or `page` object if needed: ```erb <%= inertia_root(id: 'my-app', page: custom_page_object) %> ``` ### `inertia_ssr_head` Returns the SSR head content when [server-side rendering](/guide/server-side-rendering) is enabled. This should be included in your layout's `` section to inject SSR-generated meta tags and other head elements. ```erb # app/views/layouts/inertia.html.erb <%= inertia_ssr_head %> <%= yield %> ``` ### `inertia_meta_tags` Renders meta tags that were defined server-side using the `inertia_meta` configuration. This is useful for SEO when you want to manage meta tags from your Rails controllers. See the [Server-managed meta tags cookbook](/cookbook/server-managed-meta-tags) for a complete guide. ```erb <%= inertia_meta_tags %> ``` ### Passing Additional Data to the View Sometimes you may want to provide data to the root template that will not be sent to your JavaScript page / component. This can be accomplished by passing the `view_data` option. ```ruby def show event = Event.find(params[:id]) render inertia: { event: }, view_data: { meta: event.meta } end ``` You can then access this variable like a regular local variable. ```erb # app/views/inertia.html.erb <% content_for(:head) do %> "> <% end %> <%= inertia_root %> ``` ## Rails Generators Inertia Rails provides a number of generators to help you get started with Inertia in your Rails application. You can generate controllers or use scaffolds to create a new resource with Inertia responses. ### Scaffold Generator Use the `inertia:scaffold` generator to create a resource with Inertia responses. Execute the following command in the terminal: ```bash bin/rails generate inertia:scaffold ModelName field1:type field2:type ``` Example output: ```bash $ bin/rails generate inertia:scaffold Post title:string body:text invoke active_record create db/migrate/20240611123952_create_posts.rb create app/models/post.rb invoke test_unit create test/models/post_test.rb create test/fixtures/posts.yml invoke resource_route route resources :posts invoke scaffold_controller create app/controllers/posts_controller.rb invoke inertia_templates create app/frontend/pages/posts create app/frontend/pages/posts/index.svelte create app/frontend/pages/posts/edit.svelte create app/frontend/pages/posts/show.svelte create app/frontend/pages/posts/new.svelte create app/frontend/pages/posts/form.svelte create app/frontend/pages/posts/post.svelte invoke resource_route invoke test_unit create test/controllers/posts_controller_test.rb create test/system/posts_test.rb invoke helper create app/helpers/posts_helper.rb invoke test_unit ``` #### Tailwind CSS Integration Inertia Rails tries to detect the presence of Tailwind CSS in the application and generate the templates accordingly. If you want to specify templates type, use the `--inertia-templates` option: * `inertia_templates` - default * `inertia_tw_templates` - Tailwind CSS ### Controller Generator Use the `inertia:controller` generator to create a controller with an Inertia response. Execute the following command in the terminal: ```bash bin/rails generate inertia:controller ControllerName action1 action2 ``` Example output: ```bash $ bin/rails generate inertia:controller pages welcome next_steps create app/controllers/pages_controller.rb route get 'pages/welcome' get 'pages/next_steps' invoke test_unit create test/controllers/pages_controller_test.rb invoke helper create app/helpers/pages_helper.rb invoke test_unit invoke inertia_templates create app/frontend/pages/pages create app/frontend/pages/pages/welcome.jsx create app/frontend/pages/pages/next_steps.jsx ``` ### Customizing the Generator Templates Rails generators allow templates customization. You can create custom template files in your application to override the default templates used by the generators. For example, to customize the controller generator view template for React, create a file at the path `lib/templates/inertia_templates/controller/react/view.jsx.tt`: ```jsx export default function <%= @action.camelize %>() { return (

    Hello from my new default template

    ); } ``` You can find the default templates in the gem's source code: * [Default controller generator templates](https://github.com/inertiajs/inertia-rails/tree/master/lib/generators/inertia_templates/controller/templates) * [Default scaffold generator templates](https://github.com/inertiajs/inertia-rails/tree/master/lib/generators/inertia_templates/scaffold/templates) * [Tailwind controller generator templates](https://github.com/inertiajs/inertia-rails/tree/master/lib/generators/inertia_tw_templates/controller/templates) * [Tailwind scaffold generator templates](https://github.com/inertiajs/inertia-rails/tree/master/lib/generators/inertia_tw_templates/scaffold/templates) > \[!TIP] > You can also replace the whole generator with your own implementation. See the [Rails documentation](https://guides.rubyonrails.org/generators.html#overriding-rails-generators) for more information. ## Maximum Response Size To enable client-side history navigation, all Inertia server responses are stored in the browser's history state. However, keep in mind that some browsers impose a size limit on how much data can be saved within the history state. For example, [Firefox](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) has a size limit of 16 MiB and throws a `NS_ERROR_ILLEGAL_VALUE` error if you exceed this limit. Typically, this is much more data than you'll ever practically need when building applications. ## Detecting Inertia Requests Controllers can determine if a request was made via Inertia: ```ruby def some_action if request.inertia? # This is an Inertia request end if request.inertia_partial? # This is a partial Inertia request end end ``` ## Inertia Responses and `respond_to` Inertia responses always operate as a `:html` response type. This means that you can use the `respond_to` method to handle JSON requests differently, while still returning Inertia responses: ```ruby def some_action respond_to do |format| format.html do render inertia: { data: 'value' } end format.json do render json: { message: 'This is a JSON response' } end end end ``` --- --- url: /guide/routing.md --- # Routing ## Defining routes When using Inertia, all of your application's routes are defined server-side. This means that you don't need Vue Router or React Router. Instead, you can simply define Rails routes and return Inertia responses from those routes. ## Shorthand routes If you have a page that doesn't need a corresponding controller method, like an "FAQ" or "about" page, you can route directly to a component via the `inertia` method. ```ruby # In config/routes.rb Rails.application.routes.draw do # Basic usage - maps 'dashboard' URL to 'Dashboard' component inertia 'dashboard' => 'Dashboard' # Using a symbol - infers component name from route inertia :settings # Within namespaces and scopes namespace :admin do inertia 'dashboard' => 'Admin/Dashboard' end # Within resource definitions resources :users do inertia :activity, on: :member inertia :statistics, on: :collection end end ``` ## Generating URLs Some server-side frameworks allow you to generate URLs from named routes. However, you will not have access to those helpers client-side. Here are a couple ways to still use named routes with Inertia. The first option is to generate URLs server-side and include them as props. Notice in this example how we're passing the `edit_url` and `create_url` to the `users/index` component. ```ruby class UsersController < ApplicationController def index render inertia: { users: User.all.map do |user| user.as_json( only: [ :id, :name, :email ] ).merge( edit_url: edit_user_path(user) ) end, create_url: new_user_path } end end ``` Another option is to use [JsRoutes](https://github.com/railsware/js-routes) or [JS From Routes](https://js-from-routes.netlify.app) gems that make named server-side routes available on the client via autogenerated helpers. --- --- url: /guide/scroll-management.md --- # Scroll Management ## Scroll Resetting When navigating between pages, Inertia mimics default browser behavior by automatically resetting the scroll position of the document body (as well as any [scroll regions](#scroll-regions) you've defined) back to the top. In addition, Inertia keeps track of the scroll position of each page and automatically restores that scroll position as you navigate forward and back in history. ## Scroll Preservation Sometimes it's desirable to prevent the default scroll resetting when making visits. You can disable this behavior by setting the `preserveScroll` option to `true`. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.visit(url, { preserveScroll: true }) ``` \== React ```js import { router } from '@inertiajs/react' router.visit(url, { preserveScroll: true }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.visit(url, { preserveScroll: true }) ``` ::: If you'd like to only preserve the scroll position if the response includes validation errors, set the `preserveScroll` option to `"errors"`. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.visit(url, { preserveScroll: 'errors' }) ``` \== React ```js import { router } from '@inertiajs/react' router.visit(url, { preserveScroll: 'errors' }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.visit(url, { preserveScroll: 'errors' }) ``` ::: You can also lazily evaluate the `preserveScroll` option based on the response by providing a callback. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.post('/users', data, { preserveScroll: (page) => page.props.someProp === 'value', }) ``` \== React ```js import { router } from '@inertiajs/react' router.post('/users', data, { preserveScroll: (page) => page.props.someProp === 'value', }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.post('/users', data, { preserveScroll: (page) => page.props.someProp === 'value', }) ``` ::: When using an [Inertia link](/guide/links), you can preserve the scroll position using the `preserveScroll` prop. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { Link } from '@inertiajs/react' export default () => ( Home ) ``` \== Svelte ```svelte Home Home ``` ::: ## Scroll Regions If your app doesn't use document body scrolling, but instead has scrollable elements (using the `overflow` CSS property), scroll resetting will not work. In these situations, you must tell Inertia which scrollable elements to manage by adding the `scroll-region` attribute to the element. ```html
    ``` ## Text Fragments [Text fragments](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Fragment/Text_fragments) allow you to link directly to specific text on a page using a special URL syntax like `#:~:text=term`. However, the browser removes the fragment directive before any JavaScript runs, so text fragments only work if the targeted text is present in the initial HTML response. To use text fragments with your Inertia pages, enable [server-side rendering](/guide/server-side-rendering). --- --- url: /cookbook/server-managed-meta-tags.md --- # Server Managed Meta Tags Inertia Rails can manage a page's meta tags on the server instead of on the frontend. This means that link previews (such as on Facebook, LinkedIn, etc.) will include correct meta *without server-side rendering*. Inertia Rails renders server defined meta tags into both the server rendered HTML and the client-side Inertia page props. Because the tags share unique `head-key` attributes, the client will "take over" the meta tags after the initial page load. @available\_since rails=3.10.0 ## Setup ### Server Side Simply add the `inertia_meta_tags` helper to your layout. This will render the meta tags in the `` section of your HTML. ```erb ... <%= inertia_meta_tags %> // [!code ++] My Inertia App // [!code --] ``` > \[!NOTE] > Make sure to remove the `` tag in your Rails layout if you plan to manage it with Inertia Rails. Otherwise you will end up with duplicate `<title>` tags. ### Client Side Copy the following code into your application. It should be rendered **once** in your application, such as in a [layout component ](/guide/pages#creating-layouts). :::tabs key:frameworks \== Vue ```vue <script> import { Head } from '@inertiajs/vue3' import { usePage } from '@inertiajs/vue3' import { h } from 'vue' export default { name: 'MetaTags', setup() { const page = usePage() return () => { const metaTags = page.props._inertia_meta || [] return h(Head, {}, () => metaTags.map((meta) => { const { tagName, innerContent, headKey, httpEquiv, ...attrs } = meta const attributes = { key: headKey, 'head-key': headKey, ...attrs, } if (httpEquiv) { attributes['http-equiv'] = httpEquiv } let content = null if (innerContent != null) { content = typeof innerContent === 'string' ? innerContent : JSON.stringify(innerContent) } return h(tagName, attributes, content) }), ) } }, } </script> ``` \== React ```jsx import React from 'react' import { Head, usePage } from '@inertiajs/react' const MetaTags = () => { const { _inertia_meta: meta } = usePage().props return ( <Head> {meta.map((meta) => { const { tagName, innerContent, headKey, httpEquiv, ...attrs } = meta let stringifiedInnerContent if (innerContent != null) { stringifiedInnerContent = typeof innerContent === 'string' ? innerContent : JSON.stringify(innerContent) } return React.createElement(tagName, { key: headKey, 'head-key': headKey, ...(httpEquiv ? { 'http-equiv': httpEquiv } : {}), ...attrs, ...(stringifiedInnerContent ? { dangerouslySetInnerHTML: { __html: stringifiedInnerContent } } : {}), }) })} </Head> ) } export default MetaTags ``` \== Svelte ```svelte <!-- MetaTags.svelte --> <script> import { onMount } from 'svelte' import { page } from '@inertiajs/svelte' $: metaTags = ($page.props._inertia_meta ?? []).map( ({ tagName, headKey, innerContent, httpEquiv, ...attrs }) => ({ tagName, headKey, innerContent, attrs: httpEquiv ? { ...attrs, 'http-equiv': httpEquiv } : attrs, }), ) // Svelte throws warnings if we render void elements like meta with content $: voidTags = metaTags.filter((tag) => tag.innerContent == null) $: contentTags = metaTags.filter((tag) => tag.innerContent != null) let ready = false onMount(() => { // Clean up server-rendered tags document.head.querySelectorAll('[inertia]').forEach((el) => el.remove()) ready = true }) </script> <svelte:head> {#if ready} <!-- Void elements (no content) --> {#each voidTags as tag (tag.headKey)} <svelte:element this={tag.tagName} inertia={tag.headKey} {...tag.attrs} /> {/each} <!-- Elements with content --> {#each contentTags as tag (tag.headKey)} <svelte:element this={tag.tagName} inertia={tag.headKey} {...tag.attrs}> {@html typeof tag.innerContent === 'string' ? tag.innerContent : JSON.stringify(tag.innerContent)} </svelte:element> {/each} {/if} </svelte:head> ``` ::: ## Rendering Meta Tags Tags are defined as plain hashes and conform to the following structure: ```ruby # All fields are optional. { # Defaults to "meta" if not provided tag_name: "meta", # Used for <meta http-equiv="..."> http_equiv: "Content-Security-Policy", # Used to deduplicate tags. InertiaRails will auto-generate one if not provided head_key: "csp-header", # Used with <script>, <title>, etc. inner_content: "Some content", # Any additional attributes will be passed directly to the tag. # For example: name: "description", content: "Page description" name: "description", content: "A description of the page" } ``` The `<title>` tag has shortcut syntax: ```ruby { title: "The page title" } ``` ### In the renderer Add meta tags to an action by passing an array of hashes to the `meta:` option in the `render` method: ```ruby class EventsController < ApplicationController def show event = Event.find(params[:id]) render inertia: { event: event.as_json }, meta: [ { title: "Check out the #{event.name} event!" }, { name: 'description', content: event.description }, { tag_name: 'script', type: 'application/ld+json', inner_content: { '@context': 'https://schema.org', '@type': 'Event', name: 'My Event' } } ] end end ``` ### Shared Meta Tags Often, you will want to define default meta tags that are shared across certain pages and which you can override within a specific controller or action. Inertia Rails has an `inertia_meta` controller instance method which references a store of meta tag data. You can call it anywhere in a controller to manage common meta tags, such as in `before_action` callbacks or directly in an action. ```ruby class EventsController < ApplicationController before_action :set_meta_tags def show render inertia: { event: Event.find(params[:id]) } end private def set_meta_tags inertia_meta.add([ { title: 'Look at this event!' } ]) end end ``` #### The `inertia_meta` API The `inertia_meta` method provides a simple API to manage your meta tags. You can add, remove, or clear tags as needed. The `inertia_meta.remove` method accepts either a `head_key` string or a block to filter tags. ```ruby # Add a single tag inertia_meta.add({ title: 'Some Page title' }) # Add multiple tags at once inertia_meta.add([ { tag_name: 'meta', name: 'og:description', content: 'A description of the page' }, { tag_name: 'meta', name: 'twitter:title', content: 'A title for Twitter' }, { tag_name: 'title', inner_content: 'A title for the page', head_key: 'my_custom_head_key' }, { tag_name: 'script', type: 'application/ld+json', inner_content: { '@context': 'https://schema.org', '@type': 'Event', name: 'My Event' } } ]) # Remove a specific tag by head_key inertia_meta.remove("my_custom_head_key") # Remove tags by a condition inertia_meta.remove do |tag| tag[:tag_name] == 'script' && tag[:type] == 'application/ld+json' end # Remove all tags inertia_meta.clear ``` #### JSON-LD and Script Tags Inertia Rails supports defining `<script>` tags with `type="application/ld+json"` for structured data. All other script tags will be marked as `type="text/plain"` to prevent them from executing on the client side. Executable scripts should be added either in the Rails layout or using standard techniques in your frontend framework. ```ruby inertia_meta.add({ tag_name: "script", type: "application/ld+json", inner_content: { "@context": "https://schema.org", "@type": "Event", name: "My Event", startDate: "2023-10-01T10:00:00Z", location: { "@type": "Place", name: "Event Venue", address: "123 Main St, City, Country" } } }) ``` ## Deduplication > \[!NOTE] > The Svelte adapter does not have a `<Head />` component. Inertia Rails will deduplicate meta tags *on the server*, and the Svelte component above will render them deduplicated accordingly. ### Automatic Head Keys Inertia Rails relies on the `head-key` attribute and the `<Head />` components that the Inertia.js core uses to [manage meta tags](/guide/title-and-meta) and deduplicate them. Inertia.js core expects us to manage `head-key` attributes and deduplication manually, but Inertia Rails will generate them automatically for you. * `<meta>` tags will use the `name`,`property`, or `http_equiv` attributes to generate a head key. This enables automatic deduplication of common meta tags like `description`, `og:title`, and `twitter:card`. * All other tags will deterministically generate a `head-key` based on the tag's attributes. #### Allowing Duplicates Sometimes, it is valid HTML to have multiple meta tags with the same name or property. If you want to allow duplicates, you can set the `allow_duplicates` option to `true` when defining the tag. ```ruby class StoriesController < ApplicationController before_action do inertia_meta.add({ name: 'article:author', content: 'Tony Gilroy' }) end # Renders a single article:author meta tag def single_author render inertia: 'stories/show' end # Renders multiple article:author meta tags def multiple_authors render inertia: 'stories/show', meta: [ { name: 'article:author', content: 'Dan Gilroy', allow_duplicates: true }, ] end end ``` ### Manual Head Keys Automatic head keys should cover the majority of use cases, but you can set `head_key` manually if you need to control the deduplication behavior more precisely. For example, you may want to do this if you know you will remove a shared meta tag in a specific action. ```ruby # In a concern or `before_action` callback inertia_meta.add([ { tag_name: 'meta', name: 'description', content: 'A description of the page', head_key: 'my_custom_head_key' }, ]) # Later in a specific action inertia_meta.remove('my_custom_head_key') ``` ## Combining Meta Tag Methods There are multiple ways to manage meta tags in Inertia Rails: * Adding tags to a Rails layout such as `application.html.erb`. * Using the `<Head />` component from Inertia.js (or the Svelte head element) in the frontend. * Using the server driven meta tags feature described here. Nothing prevents you from using these together, but for organizational purposes, we recommended using only one of the last two techniques. --- --- url: /guide/server-side-rendering.md --- # Server-Side Rendering (SSR) Server-side rendering pre-renders your JavaScript pages on the server, allowing your visitors to receive fully rendered HTML when they visit your application. Since fully rendered HTML is served by your application, it's also easier for search engines to index your site. Server-side rendering uses Node.js to render your pages in a background process; therefore, Node must be available on your server for server-side rendering to function properly. Inertia's SSR server requires Node.js 22 or higher. ## Vite Plugin Setup @available\_since rails=3.19.0 core=3.0.0 The recommended way to configure SSR is with the `@inertiajs/vite` plugin. This approach handles SSR configuration automatically, including development mode SSR without a separate Node.js server. ### 1. Install the Vite plugin ```bash npm install @inertiajs/vite ``` ### 2. Configure Vite Add the Inertia plugin to your `vite.config.js` file. And configure entry point. ```js // vite.config.js import inertia from '@inertiajs/vite' export default defineConfig({ plugins: [ // ... inertia({ ssr: { entry: 'entrypoints/inertia.js', }, }), ], }) ``` You may also configure SSR options explicitly. ```js // vite.config.js inertia({ ssr: { entry: 'ssr/ssr.js', port: 13714, cluster: true, }, }) ``` You may pass `false` to opt out of the plugin's automatic SSR handling, for example if you prefer to [configure SSR manually](#manual-setup) but still want to use the other features of the Vite plugin. ```js // vite.config.js inertia({ ssr: false, }) ``` ### 3. Update your build script Update the `build` script in your `package.json` to build both bundles. ```json { "scripts": { "dev": "vite", "build": "vite build" // [!code --] "build": "vite build && vite build --ssr" // [!code ++] } } ``` ### Development Mode The Vite plugin handles SSR automatically during development. There is no need to build your SSR bundle or start a separate Node.js server. Simply run your dev servers as usual: ```bash bin/dev ``` The Vite plugin exposes a server endpoint that Rails uses for rendering, complete with HMR support. > \[!NOTE] > The `vite build --ssr`, `bin/vite ssr`, etc. commands are for [production](#production) only. You should not run them during development. ### Production For production, build both bundles and start the SSR server. ```bash npm run build node public/assets-ssr/inertia.js ``` ### Custom SSR Entry Point The Vite plugin reuses your `inertia.js` entry point for both browser and SSR rendering by default, so no separate file is needed. The plugin detects the `data-server-rendered` attribute to decide whether to hydrate or mount, and the `setup` and `resolve` callbacks are optional. Most app customizations, such as registering plugins or wrapping with providers, may be handled using the [`withApp` callback](/guide/client-side-setup#customizing-the-app) in your main entry point. A separate SSR entry point is only needed when you require completely different setup logic for the server. You may create a separate `resources/js/ssr.js` file for this purpose. :::tabs key:frameworks \== Vue ```js import createServer from '@inertiajs/vue3/server' createServer((page) => createInertiaApp({ page, render: renderToString, resolve: (name) => { const pages = import.meta.glob('../pages/**/*.vue') return pages[`../pages/${name}.vue`]() }, setup({ App, props, plugin }) { return createSSRApp({ render: () => h(App, props), }).use(plugin) }, }), ) ``` \== React ```jsx import createServer from '@inertiajs/react/server' import ReactDOMServer from 'react-dom/server' createServer((page) => createInertiaApp({ page, render: ReactDOMServer.renderToString, resolve: (name) => { const pages = import.meta.glob('../pages/**/*.jsx') return pages[`../pages/${name}.jsx`]() }, setup: ({ App, props }) => <App {...props} />, }), ) ``` \== Svelte ```js import createServer from '@inertiajs/svelte/server' createServer((page) => createInertiaApp({ page, resolve: (name) => { const pages = import.meta.glob('../pages/**/*.svelte') return pages[`../pages/${name}.svelte`]() }, setup({ App, props }) { return render(App, { props }) }, }), ) ``` ::: Be sure to add anything that's missing from your `inertia.js` file that makes sense to run in SSR mode, such as plugins or custom mixins. ## Manual Setup If you prefer not to use the Vite plugin, you may configure SSR manually. ### 1. Create an SSR entry point Create an SSR entry point file within your Laravel project. :::tabs key:frameworks \== Vue ```bash touch app/frontend/ssr/ssr.js ``` \== React ```bash touch app/frontend/ssr/ssr.jsx ``` \== Svelte ```bash touch app/frontend/ssr/ssr.js ``` ::: This file will look similar to your app entry point, but it runs in Node.js instead of the browser. Here's a complete example. :::tabs key:frameworks \== Vue ```js import createServer from '@inertiajs/vue3/server' createServer((page) => createInertiaApp({ page, render: renderToString, resolve: (name) => { const pages = import.meta.glob('../pges/**/*.vue') return pages[`../pages/${name}.vue`]() }, setup({ App, props, plugin }) { return createSSRApp({ render: () => h(App, props), }).use(plugin) }, }), ) ``` \== React ```jsx import createServer from '@inertiajs/react/server' import ReactDOMServer from 'react-dom/server' createServer((page) => createInertiaApp({ page, render: ReactDOMServer.renderToString, resolve: (name) => { const pages = import.meta.glob('../pges/**/*.jsx') return pages[`../pages/${name}.jsx`]() }, setup: ({ App, props }) => <App {...props} />, }), ) ``` \== Svelte ```js import createServer from '@inertiajs/svelte/server' createServer((page) => createInertiaApp({ page, resolve: (name) => { const pages = import.meta.glob('../pges/**/*.svelte') return pages[`../pages/${name}.svelte`]() }, setup({ App, props }) { return render(App, { props }) }, }), ) ``` ::: ### 2. Configure Vite Next, we need to update our Vite configuration to build our new `ssr.js` file. We can do this by adding a `ssrBuildEnabled` property to Ruby Vite plugin configuration in the `config/vite.json` file. ```json "production": { "ssrBuildEnabled": true } ``` ### 3. Enable SSR in the Inertia's Rails adapter Update Inertia Rails adapter cinfig to turn SSR on. ```ruby # config/initializers/inertia_rails.rb InertiaRails.configure do |config| config.ssr_enabled = ViteRuby.config.ssr_build_enabled end ``` ### Clustering By default, the SSR server runs on a single thread. You may enable clustering to start multiple Node servers on the same port, with requests handled by each thread in a round-robin fashion. ```js // vite.config.js inertia({ ssr: { cluster: true, }, }) ``` When using a [custom SSR entry point](#custom-ssr-entry-point) or [manual setup](#manual-setup), you may pass the `cluster` option to `createServer` instead. @available\_since core=2.0.7 :::tabs key:frameworks \== Vue ```js createServer( (page) => createInertiaApp({ // ... }), { cluster: true }, ) ``` \== React ```jsx createServer( (page) => createInertiaApp({ // ... }), { cluster: true }, ) ``` \== Svelte ```js createServer( (page) => createInertiaApp({ // ... }), { cluster: true }, ) ``` ::: ## Running the SSR Server > \[!NOTE] > The SSR server is only required in production. During development, the [Vite plugin](#development-mode) handles SSR automatically. ### Puma Plugin @available\_since rails=3.20.0 The recommended way to run the SSR server in production is with the built-in Puma plugin. Add the plugin to your Puma configuration: ```ruby # config/puma.rb plugin :inertia_ssr ``` The plugin automatically starts and stops the SSR Node.js process alongside Puma. It handles health checks, automatic restarts on crashes, and graceful shutdown. No separate process management (systemd, Procfile, etc.) is needed. The plugin is a no-op when `ssr_enabled` is `false` or the SSR bundle is not found, so it is safe to add unconditionally. #### Bundle Resolution The plugin locates the SSR bundle using the following rules, in order: 1. **Explicit config** — `config.ssr_bundle` (a path or an array of paths; the first existing file wins). 2. **ViteRuby output** — if ViteRuby is loaded, it globs `<ssr_output_dir>/*.js`. 3. **`ssr/` directory** — globs `ssr/*.js` in the project root (matches the `@inertiajs/vite` plugin's default SSR build output). 4. **Legacy fallback** — checks `public/assets-ssr/*.js`. If none of the above finds a file, the plugin logs nothing and stays idle. #### Runtime Detection The JavaScript runtime is auto-detected from your lockfile (`bun.lockb` → Bun, `deno.lock` → Deno, otherwise Node.js). To override: ```ruby # config/initializers/inertia_rails.rb InertiaRails.configure do |config| config.ssr_runtime = "bun" end ``` ### Manual If you are not using Puma, or prefer to manage the SSR process yourself, start the SSR server manually: ```bash bin/vite ssr ``` With the server running, you should be able to access your app within the browser with server-side rendering enabled. In fact, you should be able to disable JavaScript entirely and still navigate around your application. ## Client-Side Hydration You should also update your `inertia.js` file to use hydration instead of normal rendering. This allows VueReactSvelte to pick up the server-rendered HTML and make it interactive without re-rendering it. :::tabs key:frameworks \== Vue ```js import { createApp, h } from 'vue' // [!code --] import { createSSRApp, h } from 'vue' // [!code ++] createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('../pages/**/*.vue') return pages[`../pages/${name}.vue`]() }, setup({ el, App, props, plugin }) { createApp({ render: () => h(App, props) }) // [!code --] createSSRApp({ render: () => h(App, props) }) // [!code ++] .use(plugin) .mount(el) }, }) ``` \== React ```js import { createRoot } from 'react-dom/client' // [!code --] import { hydrateRoot } from 'react-dom/client' // [!code ++] createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('../pages/**/*.jsx') return pages[`../pages/${name}.jsx`]() }, setup({ el, App, props }) { createRoot(el).render(<App {...props} />) // [!code --] hydrateRoot(el, <App {...props} />) // [!code ++] }, }) ``` \== Svelte ```js import { mount } from 'svelte' // [!code --] import { hydrate, mount } from 'svelte' // [!code ++] createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('../pages/**/*.svelte') return pages[`../pages/${name}.svelte`]() }, setup({ el, App, props }) { mount(App, { target: el, props }) // [!code --] if (el.dataset.serverRendered === 'true') { // [!code ++:5] hydrate(App, { target: el, props }) } else { mount(App, { target: el, props }) } }, }) ``` ::: ## Error Handling When SSR rendering fails, Inertia gracefully falls back to client-side rendering. The Vite plugin logs detailed error information to the console, including the component name, request URL, source location, and a tailored hint to help you resolve the issue. Common SSR errors are automatically classified. Browser API errors (such as referencing `window` or `document` in server-rendered code) include guidance on moving the code to a lifecycle hook. Component resolution errors suggest checking file paths and casing. The Rails adapter automatically logs SSR failures to `Rails.logger` at the `error` level. To customize error handling, set the `on_ssr_error` option in your `config/initializers/inertia_rails.rb` file. ```ruby # config/initializers/inertia_rails.rb InertiaRails.configure do |config| config.on_ssr_error = ->(error, page) do Rails.logger.warn("SSR failed for #{page[:component]}: #{error.message}") Sentry.capture_exception(error) # or any error tracker end end ``` The callback receives an `InertiaRails::SSRError` and the page hash, giving you access to the component name, props, and URL that failed. ### Raising on Error For CI or E2E testing, you may prefer SSR failures to raise an exception instead of falling back silently. Set the `ssr_raise_on_error` option in your initializer. ```ruby # config/initializers/inertia_rails.rb InertiaRails.configure do |config| config.ssr_raise_on_error = true end ``` ## SSR Response Caching @available\_since rails=3.19.0 SSR rendering sends a request to the Node.js server for every page load. You can cache these responses to avoid redundant renders when the same page data is served repeatedly. ### Enabling SSR Caching Set `ssr_cache` in your initializer: ```ruby # config/initializers/inertia_rails.rb InertiaRails.configure do |config| config.ssr_cache = true end ``` When enabled, the SSR response is cached using an MD5 digest of the page JSON as the cache key. Identical page data produces the same key, so the Node.js server is only called once per unique page. ### Cache Options Pass a hash to control cache behavior: ```ruby InertiaRails.configure do |config| config.ssr_cache = { expires_in: 1.hour } end ``` ### Dynamic Configuration Use a lambda for per-request control. The lambda is evaluated in the controller context: ```ruby InertiaRails.configure do |config| config.ssr_cache = -> { { expires_in: action_name == 'index' ? 1.hour : 5.minutes } } end ``` ### Per-Render Override Override the global setting on individual render calls: ```ruby class PostsController < ApplicationController def show render inertia: 'Posts/Show', props: { post: @post.as_json }, ssr_cache: false # skip caching for this render end end ``` ### Development Mode SSR caching is automatically disabled when the Vite dev server is running, since dev responses change frequently and should not be cached. ### Cache Store SSR caching uses the same [`cache_store`](/guide/configuration#cache_store) as [cached props](/guide/cached-props). SSR cache keys are prefixed with `inertia_ssr/`. ## Disabling SSR Sometimes you may wish to disable server-side rendering for certain controllers or pages in your application. You may do so by setting the `ssr_enabled` option to `false` using `inertia_config`. ```ruby class AdminController < ApplicationController inertia_config(ssr_enabled: false) end ``` You can also use a lambda for conditional SSR, which is evaluated per-request in the controller context: ```ruby class DashboardController < ApplicationController inertia_config(ssr_enabled: -> { !complex_client_only_page? }) end ``` ## Deployment When deploying your SSR enabled app to production, build both the client-side (`application.js`) and server-side bundles (`ssr.js`), and then run the SSR server as a background process — either via the [Puma plugin](#puma-plugin) or manually. > \[!NOTE] > The Puma plugin expects Node.js (or your configured runtime) to be available in the same environment as Puma. For containerized deployments where the SSR server runs in a separate container, skip the plugin and point `ssr_url` to the SSR container instead. --- --- url: /guide/server-side-setup.md --- # Server-Side Setup The first step when installing Inertia is to configure your server-side framework. > \[!NOTE] > For the official Laravel adapter instructions, please see the [official documentation](https://inertiajs.com/server-side-setup). ## Install Dependencies First, install the Inertia server-side adapter gem and add to the application's Gemfile by executing: ```bash bundle add inertia_rails ``` ## Rails Generator If you plan to use Vite as your frontend build tool, you can use the built-in generator to install and set up Inertia in a Rails application. It automatically detects if the [Vite Rails](https://vite-ruby.netlify.app/guide/rails.html) gem is installed and will attempt to install it if not present. To install and setup Inertia in a Rails application, execute the following command in the terminal: ```bash bin/rails generate inertia:install ``` This command will: * Check for Vite Rails and install it if not present * Ask if you want to use TypeScript * Ask you to choose your preferred frontend framework (React, Vue, Svelte) * Ask if you want to install Tailwind CSS * Install necessary dependencies * Set up the application to work with Inertia * Copy example Inertia controller and views (can be skipped with the `--skip-example` option) With that done, you can now start the Rails server and the Vite development server (we recommend using [Overmind](https://github.com/DarthSim/overmind)): ```bash bin/dev ``` And navigate to `http://localhost:3100/inertia-example` to see the example Inertia page. That's it! You're all set up to start using Inertia in your Rails application. Check the guide on [creating pages](/guide/pages) to know more. ## Root Template If you decide not to use the generator, you can manually set up Inertia in your Rails application. First, setup the root template that will be loaded on the first page visit. This will be used to load your site assets (CSS and JavaScript), and will also contain a root `<div>` to boot your JavaScript application in. :::tabs key:builders \== Vite ```erb <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <%= csp_meta_tag %> <%= inertia_ssr_head %> <%# If you want to use React add `vite_react_refresh_tag` %> <%= vite_client_tag %> <%= vite_javascript_tag 'application' %> </head> <body> <%= yield %> </body> </html> ``` \== Webpacker/Shakapacker ```erb <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <%= csp_meta_tag %> <%= inertia_ssr_head %> <%= stylesheet_pack_tag 'application' %> <%= javascript_pack_tag 'application', defer: true %> </head> <body> <%= yield %> </body> </html> ``` ::: This template should include your assets, as well as the `yield` method to render the Inertia page. The `inertia_ssr_head` method is used to include the Inertia headers in the response, it's required when [SSR](/guide/server-side-rendering.md) is enabled. Inertia's adapter will use standard Rails layout inheritance, with `view/layouts/application.html.erb` as a default layout. If you would like to use a different default layout, you can change it using the `InertiaRails.configure`. ```ruby # config/initializers/inertia_rails.rb InertiaRails.configure do |config| config.layout = 'my_inertia_layout' end ``` # Creating Responses That's it, you're all ready to go server-side! Once you setup the [client-side](/guide/client-side-setup.md) framework, you can start start creating Inertia [pages](/guide/pages.md) and rendering them via [responses](/guide/responses.md). ```ruby class EventsController < ApplicationController def show event = Event.find(params[:id]) render inertia: { event: event.as_json( only: [:id, :title, :start_date, :description] ) } end end ``` --- --- url: /guide/shared-data.md --- # Shared Data Sometimes you need to access specific pieces of data on numerous pages within your application. For example, you may need to display the current user in the site header. Passing this data manually in each response across your entire application is cumbersome. Thankfully, there is a better option: shared data. ## Sharing Data The `inertia_share` method allows you to define data that will be available to all controller actions, automatically merging with page-specific props. ### Basic Usage ```ruby class EventsController < ApplicationController # Static sharing: Data is evaluated immediately inertia_share app_name: Rails.configuration.app_name # Dynamic sharing: Data is evaluated at render time inertia_share do { user: current_user&.as_json(only: [:id, :name, :email]), notifications: current_user&.unread_notifications_count } if user_signed_in? end # Alternative syntax for single dynamic values inertia_share total_users: -> { User.count } end ``` ### Inheritance and Shared Data Shared data defined in parent controllers is automatically inherited by child controllers. Child controllers can also override or add to the shared data: ```ruby # Parent controller class ApplicationController < ActionController::Base inertia_share app_name: 'My App', version: '1.0' end # Child controller class UsersController < ApplicationController # Inherits app_name and version, adds/overrides auth inertia_share auth: -> { { user: current_user&.as_json(only: [:id, :name, :email]) } } end ``` ### Conditional Sharing You can control when data is shared using Rails-style controller filters. The `inertia_share` method supports these filter options: * `only`: Share data for specific actions * `except`: Share data for all actions except specified ones * `if`: Share data when condition is true * `unless`: Share data when condition is false ```ruby class EventsController < ApplicationController # Share user data only when authenticated inertia_share if: :user_signed_in? do { user: { name: current_user.name, email: current_user.email, role: current_user.role } } end # Share data only for specific actions inertia_share only: [:index, :show] do { meta: { last_updated: Time.current, version: "1.0" } } end end ``` > \[!NOTE] > Shared data should be used sparingly as all shared data is included with every response. > \[!NOTE] > Page props and shared data are merged together, so be sure to namespace your shared data appropriately to avoid collisions. ## Sharing Once Props @available\_since rails=3.15.0 core=2.2.20 You may share data that is resolved only once and remembered by the client across subsequent navigations using [once props](/guide/once-props). ```ruby class ApplicationController < ActionController::Base inertia_share countries: InertiaRails.once { Country.all } end ``` For more information on once props, see the [once props](/guide/once-props) documentation. ## Accessing Shared Data Once you have shared the data server-side, you will be able to access it within any of your pages or components. Here's an example of how to access shared data in a layout component. :::tabs key:frameworks \== Vue ```vue <script setup> import { computed } from 'vue' import { usePage } from '@inertiajs/vue3' const page = usePage() const user = computed(() => page.props.auth.user) </script> <template> <main> <header>You are logged in as: {{ user.name }}</header> <article> <slot /> </article> </main> </template> ``` \== React ```jsx import { usePage } from '@inertiajs/react' export default function Layout({ children }) { const { auth } = usePage().props return ( <main> <header>You are logged in as: {auth.user.name}</header> <article>{children}</article> </main> ) } ``` \== Svelte ```svelte <script> import { page } from '@inertiajs/svelte' </script> <main> <header> You are logged in as: {page.props.auth.user.name} </header> <article> <slot /> </article> </main> ``` ::: ## TypeScript You may configure the shared props type globally using [TypeScript's declaration merging](/guide/typescript#shared-page-props). ## Flash Data @available\_since rails=3.17.0 core=2.3.3 For one-time notifications like toast messages or success alerts, you may use [flash data](/guide/flash-data). Unlike shared data, flash data is not persisted in the browser's history state, so it won't reappear when navigating through history. ## Deep Merging Shared Data By default, Inertia will shallow merge data defined in an action with the shared data. You might want a deep merge. Imagine using shared data to represent defaults you'll override sometimes. ```ruby class ApplicationController inertia_share do { basketball_data: { points: 50, rebounds: 100 } } end end ``` Let's say we want a particular action to change only part of that data structure. The renderer accepts a `deep_merge` option: ```ruby class CrazyScorersController < ApplicationController def index render inertia: { basketball_data: { points: 100 } }, deep_merge: true end end # The renderer will send this to the frontend: { basketball_data: { points: 100, rebounds: 100, } } ``` Deep merging can be set as the project wide default via the `InertiaRails` configuration: ```ruby # config/initializers/some_initializer.rb InertiaRails.configure do |config| config.deep_merge_shared_data = true end ``` If deep merging is enabled by default, it's possible to opt out within the action: ```ruby class CrazyScorersController < ApplicationController inertia_share do { basketball_data: { points: 50, rebounds: 10, } } end def index render inertia: { basketball_data: { points: 100 } }, deep_merge: false end end # Even if deep merging is set by default, since the renderer has `deep_merge: false`, it will send a shallow merge to the frontend: { basketball_data: { points: 100, } } ``` --- --- url: /guide/testing.md --- # Testing There are many different ways to test an Inertia application. This page provides a quick overview of the tools available. ## End-to-end Tests One popular approach to testing your JavaScript page components is to use an end-to-end testing tool like [Cypress](https://www.cypress.io/) or [Pest](https://pestphp.com). These are browser automation tools that allow you to run real simulations of your app in the browser. These tests are known to be slower; however, since they test your application at the same layer as your end users, they can provide a lot of confidence that your app is working correctly. And, since these tests are run in the browser, your JavaScript code is actually executed and tested as well. ## Client-Side Unit Tests Another approach to testing your page components is using a client-side unit testing framework, such as [Vitest](https://vitest.dev/), [Jest](https://jestjs.io/), or [Mocha](https://mochajs.org/). This approach allows you to test your JavaScript page components in isolation using Node.js. ## Endpoint Tests @available\_since rails=3.17.0 Inertia Rails provides test helpers for both RSpec and Minitest. :::tabs key:tests \== RSpec ```ruby # spec/rails_helper.rb require 'inertia_rails/rspec' ``` \== Minitest ```ruby # test/test_helper.rb require 'inertia_rails/minitest' ``` ::: RSpec helpers are automatically available in all request specs. Minitest helpers are automatically included in `ActionDispatch::IntegrationTest`. ## Assertions Both RSpec and Minitest provide matchers/assertions for testing Inertia responses. In RSpec, negation is done with `not_to`. The `inertia` helper gives you direct access to `inertia.props`, `inertia.component`, `inertia.view_data`, `inertia.flash`, and `inertia.deferred_props`. | Description | RSpec | Minitest | | ------------------------- | ---------------------- | ------------------------------------------------------------------- | | Inertia response | `be_inertia_response` | `assert_inertia_response` / `refute_inertia_response` | | Component name | `render_component` | `assert_inertia_component` / `refute_inertia_component` | | Props (partial match) | `have_props` | `assert_inertia_props` / `refute_inertia_props` | | Props (exact match) | `have_exact_props` | `assert_inertia_props_equal` / `refute_inertia_props_equal` | | Prop key absent | `have_no_prop` | `assert_no_inertia_prop` | | View data (partial match) | `have_view_data` | `assert_inertia_view_data` / `refute_inertia_view_data` | | View data (exact match) | `have_exact_view_data` | `assert_inertia_view_data_equal` / `refute_inertia_view_data_equal` | | View data key absent | `have_no_view_data` | `assert_no_inertia_view_data` | | Flash (partial match) | `have_flash` | `assert_inertia_flash` / `refute_inertia_flash` | | Flash (exact match) | `have_exact_flash` | `assert_inertia_flash_equal` / `refute_inertia_flash_equal` | | Flash key absent | `have_no_flash` | `assert_no_inertia_flash` | | Deferred props | `have_deferred_props` | `assert_inertia_deferred_props` / `refute_inertia_deferred_props` | :::tabs key:tests \== RSpec ```ruby # spec/requests/events_spec.rb RSpec.describe '/events' do describe '#index' do let!(:event) { Event.create!(title: 'Rails World', start_date: '2026-09-23', description: 'Annual Ruby on Rails conference') } it 'renders inertia component' do get events_path expect(inertia).to be_inertia_response expect(inertia).to render_component 'events/index' expect(inertia).to have_props(title: 'Rails World') expect(inertia).to have_exact_props(title: 'Rails World', start_date: '2026-09-23', description: 'Annual Ruby on Rails conference') # Props support both symbol and string keys expect(inertia.props[:title]).to eq 'Rails World' expect(inertia.props['title']).to eq 'Rails World' expect(inertia).to have_view_data(timezone: 'UTC') expect(inertia).to have_exact_view_data(timezone: 'UTC') expect(inertia.view_data[:timezone]).to eq 'UTC' expect(inertia).to have_no_prop(:secret) end end end ``` \== Minitest ```ruby # test/integration/events_test.rb class EventsTest < ActionDispatch::IntegrationTest test 'renders inertia component' do event = Event.create!(title: 'Rails World', start_date: '2026-09-23', description: 'Annual Ruby on Rails conference') get events_path assert_inertia_response assert_inertia_component 'events/index' assert_inertia_props title: 'Rails World' assert_inertia_props_equal title: 'Rails World', start_date: '2026-09-23', description: 'Annual Ruby on Rails conference' # Props support both symbol and string keys assert_equal 'Rails World', inertia.props[:title] assert_equal 'Rails World', inertia.props['title'] assert_inertia_view_data timezone: 'UTC' assert_inertia_view_data_equal timezone: 'UTC' assert_equal 'UTC', inertia.view_data[:timezone] assert_no_inertia_view_data :secret assert_no_inertia_prop :secret end end ``` ::: ## Common Testing Tasks ### Test Flash Messages Inertia Rails automatically shares [flash data](/guide/flash-data) with your frontend. :::tabs key:tests \== RSpec ```ruby RSpec.describe '/events' do it 'shows flash message after create' do post events_path, params: { event: { title: 'New Event' } } expect(inertia).to have_flash(notice: 'Event created!') expect(inertia).to have_exact_flash(notice: 'Event created!') expect(inertia.flash[:notice]).to eq 'Event created!' expect(inertia).to have_no_flash(:alert) end end ``` \== Minitest ```ruby class EventsTest < ActionDispatch::IntegrationTest test 'shows flash message after create' do post events_path, params: { event: { title: 'New Event' } } assert_inertia_flash notice: 'Event created!' assert_inertia_flash_equal notice: 'Event created!' assert_equal 'Event created!', inertia.flash[:notice] assert_no_inertia_flash :alert end end ``` ::: ### Test Validation Errors [Validation errors](/guide/validation) are shared as props automatically when using `redirect_to` with `inertia_errors`. Assert them on the `errors` key. :::tabs key:tests \== RSpec ```ruby RSpec.describe '/events' do it 'returns validation errors' do post events_path, params: { event: { title: '' } } expect(inertia).to render_component 'events/new' expect(inertia).to have_props(errors: { title: ["can't be blank"] }) end end ``` \== Minitest ```ruby class EventsTest < ActionDispatch::IntegrationTest test 'returns validation errors' do post events_path, params: { event: { title: '' } } assert_inertia_component 'events/new' assert_inertia_props errors: { title: ["can't be blank"] } end end ``` ::: ### Test Redirects After a [redirect](/guide/redirects), use `follow_redirect!` and assert the resulting Inertia response. :::tabs key:tests \== RSpec ```ruby RSpec.describe '/events' do it 'redirects after create' do post events_path, params: { event: { title: 'Conference' } } follow_redirect! expect(inertia).to render_component 'events/show' expect(inertia).to have_flash(notice: 'Event created!') end end ``` \== Minitest ```ruby class EventsTest < ActionDispatch::IntegrationTest test 'redirects after create' do post events_path, params: { event: { title: 'Conference' } } follow_redirect! assert_inertia_component 'events/show' assert_inertia_flash notice: 'Event created!' end end ``` ::: ### Test Deferred Props [Deferred props](/guide/deferred-props) are excluded from the initial page load and fetched in a subsequent request. :::tabs key:tests \== RSpec ```ruby RSpec.describe '/events' do it 'defers expensive data' do get events_path expect(inertia).to have_deferred_props expect(inertia).to have_deferred_props(:analytics) expect(inertia).to have_deferred_props(:analytics, :statistics) # Check a specific group expect(inertia).to have_deferred_props(:other_data, group: :slow) expect(inertia.deferred_props[:default]).to include('analytics') expect(inertia.props[:analytics]).to be_nil end end ``` \== Minitest ```ruby class EventsTest < ActionDispatch::IntegrationTest test 'defers expensive data' do get events_path assert_inertia_deferred_props assert_inertia_deferred_props :analytics assert_inertia_deferred_props :analytics, :statistics # Check a specific group assert_inertia_deferred_props :other_data, group: :slow assert_includes inertia.deferred_props[:default], 'analytics' assert_nil inertia.props[:analytics] end end ``` ::: ### Test Partial Reloads Use `inertia_reload_only`, `inertia_reload_except`, and `inertia_load_deferred_props` to simulate [partial reloads](/guide/partial-reloads) and deferred prop loading. :::tabs key:tests \== RSpec ```ruby RSpec.describe '/events' do it 'supports partial reloads' do get events_path inertia_reload_only(:analytics, :statistics) expect(inertia.props[:analytics]).to be_present inertia_reload_except(:expensive_data) # Load deferred props by group inertia_load_deferred_props(:default) # Load all deferred props inertia_load_deferred_props end end ``` \== Minitest ```ruby class EventsTest < ActionDispatch::IntegrationTest test 'supports partial reloads' do get events_path inertia_reload_only(:analytics, :statistics) assert_not_nil inertia.props[:analytics] inertia_reload_except(:expensive_data) # Load deferred props by group inertia_load_deferred_props(:default) # Load all deferred props inertia_load_deferred_props end end ``` ::: ## Configuration ### `evaluate_optional_props` @available\_since rails=3.18.0 By default, [optional](/guide/partial-reloads#optional-props) and [deferred](/guide/deferred-props) props are excluded on first load — just like in production. This means `inertia.props[:my_optional]` returns `nil` unless you simulate a partial reload. To have these props evaluated on first load in tests, enable `evaluate_optional_props`: :::tabs key:tests \== RSpec ```ruby # spec/rails_helper.rb require 'inertia_rails/rspec' InertiaRails::Testing.evaluate_optional_props = true ``` \== Minitest ```ruby # test/test_helper.rb require 'inertia_rails/minitest' InertiaRails::Testing.evaluate_optional_props = true ``` ::: Optional and deferred props are then included in `inertia.props` on first load: :::tabs key:tests \== RSpec ```ruby get events_path expect(inertia.props[:analytics]).to be_present expect(inertia.props[:statistics]).to eq({ views: 100 }) ``` \== Minitest ```ruby get events_path assert_not_nil inertia.props[:analytics] assert_equal({ views: 100 }, inertia.props[:statistics]) ``` ::: You can also toggle this setting per-test: :::tabs key:tests \== RSpec ```ruby around do |example| InertiaRails::Testing.evaluate_optional_props = true example.run ensure InertiaRails::Testing.evaluate_optional_props = false end ``` \== Minitest ```ruby def test_optional_props InertiaRails::Testing.evaluate_optional_props = true get events_path assert_not_nil inertia.props[:analytics] ensure InertiaRails::Testing.evaluate_optional_props = false end ``` ::: ::: warning When `evaluate_optional_props` is enabled, deferred props will appear in `inertia.props` but will still be listed in `inertia.deferred_props`. Partial reload behaviour is unaffected — this setting only changes first-load behaviour. ::: --- --- url: /guide/the-protocol.md --- # The Protocol This page contains a detailed specification of the Inertia protocol. Be sure to read the [how it works](/guide/how-it-works) page first for a high-level overview. ## HTML Responses The very first request to an Inertia app is just a regular, full-page browser request, with no special Inertia headers or data. For these requests, the server returns a full HTML document. This HTML response includes the site assets (CSS, JavaScript) as well as a root `<div>` in the page's body. The root `<div>` serves as a mounting point for the client-side app. A `<script type="application/json">` element contains the JSON encoded [page object](#the-page-object) for the initial page. Inertia uses this information to boot your client-side framework and display the initial page component. ```http REQUEST GET: https://example.com/events/80 Accept: text/html, application/xhtml+xml RESPONSE HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 <html> <head> <title>My app
    ``` While the initial response is HTML, Inertia does not server-side render the JavaScript page components. For information on server-side rendering, see the [SSR documentation](/guide/server-side-rendering). ## Inertia Responses Once the Inertia app has been booted, all subsequent requests to the site are made via XHR with a `X-Inertia` header set to `true`. This header indicates that the request is being made by Inertia and isn't a standard full-page visit. When the server detects the `X-Inertia` header, instead of responding with a full HTML document, it returns a JSON response with an encoded [page object](#the-page-object). ```http REQUEST GET: https://example.com/events/80 Accept: text/html, application/xhtml+xml X-Requested-With: XMLHttpRequest X-Inertia: true X-Inertia-Version: 6b16b94d7c51cbe5b1fa42aac98241d5 RESPONSE HTTP/1.1 200 OK Content-Type: application/json Vary: X-Inertia X-Inertia: true { "component": "Event", "props": { "errors": {}, "event": { "id": 80, "title": "Birthday party", "start_date": "2019-06-02", "description": "Come out and celebrate Jonathan's 36th birthday party!" } }, "url": "/events/80", "version": "6b16b94d7c51cbe5b1fa42aac98241d5", "encryptHistory": true } ``` ## Request Lifecycle Diagram The diagram below illustrates the request lifecycle within an Inertia application. The initial visit generates a standard request to the server, which returns an HTML application skeleton containing a root element with hydrated data. For subsequent user interactions and navigation, Inertia sends XHR requests that return JSON data. Inertia uses this response to dynamically hydrate and swap the page component without a full page reload. ```mermaid sequenceDiagram participant Client participant Server Client->>Server: First Visit Server-->>Client: Returns HTML Skeleton Note over Client: Inertia.js is loaded Client->>Server: Inertia Request (X-Inertia: true) Server-->>Client: Returns JSON Payload (Component Name, Props, etc.) Note over Client: Inertia.js swaps components ``` ## Request Headers The following headers are automatically sent by Inertia when making requests. You don't need to set these manually, they're handled by the Inertia client-side adapter. * `X-Inertia`: Set to `true` to indicate this is an Inertia request. * `X-Requested-With`: Set to `XMLHttpRequest` on all Inertia requests. * `Accept`: Set to `text/html, application/xhtml+xml` to indicate acceptable response types. * `X-Inertia-Version`: The current asset version to check for asset mismatches. * `Purpose`: Set to `prefetch` when making [prefetch](/guide/prefetching) requests. * `X-Inertia-Partial-Component`: The component name for [partial reloads](/guide/partial-reloads). * `X-Inertia-Partial-Data`: Comma-separated list of props to include in partial reloads. * `X-Inertia-Partial-Except`: Comma-separated list of props to exclude from partial reloads. * `X-Inertia-Reset`: Comma-separated list of props to reset on navigation. * `Cache-Control`: Set to `no-cache` for reload requests to prevent serving stale content. * `X-Inertia-Error-Bag`: Specifies which error bag to use for [validation errors](/guide/validation). Note: This header is not used by the Rails adapter as error bags are a Laravel-specific feature. * `X-Inertia-Infinite-Scroll-Merge-Intent`: Indicates whether the requested data should be appended or prepended when using [Infinite scroll](/guide/infinite-scroll). * `X-Inertia-Except-Once-Props`: Comma-separated list of non-expired [once prop](/guide/once-props) keys already loaded on the client. The server will skip resolving these props unless explicitly requested via a partial reload or force refreshed server-side. The following headers are used for [Precognition](/guide/forms#precognition) validation requests. * `Precognition`: Set to `true` to indicate this is a Precognition validation request. * `Precognition-Validate-Only`: Comma-separated list of field names to validate. ## Response Headers The following headers should be sent by your server-side adapter in Inertia responses. If you're using an official server-side adapter, these are handled automatically. * `X-Inertia`: Set to `true` to indicate this is an Inertia response. * `X-Inertia-Location`: Used for external redirects when a `409 Conflict` response is returned due to asset version mismatches. Triggers a full `window.location` visit. * `X-Inertia-Redirect`: Used for redirects containing URL fragments when a `409 Conflict` response is returned. Contains the full redirect URL including the fragment. Triggers a standard Inertia visit instead of a full page reload. * `Vary`: Set to `X-Inertia` to help browsers correctly differentiate between HTML and JSON responses. The following headers are used for [Precognition](/guide/forms#precognition) validation responses. * `Precognition`: Set to `true` to indicate this is a Precognition validation response. * `Precognition-Success`: Set to `true` when validation passes with no errors, combined with a `204 No Content` status code. * `Vary`: Set to `Precognition` on all responses when the Precognition middleware is applied. ## The Page Object Inertia shares data between the server and client via a page object. This object includes the necessary information required to render the page component, update the browser's history state, and track the site's asset version. The page object can include the following properties: * `component`: The name of the JavaScript page component. * `props`: The page props. Contains all of the page data along with an `errors` object (defaults to `{}` if there are no errors). * `url`: The page URL. * `version`: The current [asset version](/guide/asset-versioning). * `encryptHistory`: Whether or not to [encrypt the current page's history state](/guide/history-encryption). Only included when `true`. * `clearHistory`: Whether or not to clear any [encrypted history state](/guide/history-encryption#clearing-history). Only included when `true`. * `preserveFragment`: Whether to [preserve the URL fragment](/guide/redirects#preserving-fragments) from the original request across a redirect. * `mergeProps`: Array of prop keys that should be [merged](/guide/merging-props) (appended) during navigation. * `prependProps`: Array of prop keys that should be [prepended](/guide/merging-props) during navigation. * `deepMergeProps`: Array of prop keys that should be [deep merged](/guide/merging-props#deep-merge) during navigation. * `matchPropsOn`: Array of prop keys to use for [matching when merging props](/guide/merging-props#matching-items). * `scrollProps`: Configuration for [infinite scroll](/guide/infinite-scroll) prop merging behavior. * `deferredProps`: Configuration for client-side [lazy loading of props](/guide/deferred-props). * `sharedProps`: Array of top-level prop keys registered via `Inertia::share()`. Used by the client to carry shared props over during [instant visits](/guide/instant-visits). * `onceProps`: Configuration for [once props](/guide/once-props) that should only be resolved once and reused on subsequent pages. Each entry maps a key to an object containing the `prop` name and optional `expiresAt` timestamp (in milliseconds). On standard full page visits, the page object is JSON encoded into a ` ``` \== React ```jsx import { Head } from '@inertiajs/react' export default () => ( Your page title ) ``` \== Svelte ```svelte Your page title ``` ::: ## Title Shorthand If you only need to add a `` to the document `<head>`, you may simply pass the title as a prop to the `<Head>` component. :::tabs key:frameworks \== Vue ```vue <script setup> import { Head } from '@inertiajs/vue3' </script> <template> <Head title="Your page title" /> </template> ``` \== React ```jsx import { Head } from '@inertiajs/react' export default () => <Head title="Your page title" /> ``` \== Svelte ```js // Not supported ``` ::: ## Title Callback You can globally modify the page `<title>` using the `title` callback in the `createInertiaApp` setup method. Typically, this method is invoked in your application's main JavaScript file. A common use case for the title callback is automatically adding an app name before or after each page title. ```js createInertiaApp({ title: (title) => `${title} - My App`, // ... }) ``` After defining the `title` callback, the callback will automatically be invoked when you set a title using the `<Head>` component. :::tabs key:frameworks \== Vue ```vue <script setup> import { Head } from '@inertiajs/vue3' </script> <template> <Head title="Home" /> </template> ``` \== React ```jsx import { Head } from '@inertiajs/react' export default () => <Head title="Home" /> ``` \== Svelte ```js // Not supported ``` ::: Which, in this example, will result in the following `<title>` tag. ```html <title>Home - My App ``` The `title` callback will also be invoked when you set the title using a `` tag within your `<Head>` component. :::tabs key:frameworks \== Vue ```vue <script setup> import { Head } from '@inertiajs/vue3' </script> <template> <Head> <title>Home ``` \== React ```jsx import { Head } from '@inertiajs/react' export default () => ( Home ) ``` \== Svelte ```js // Not supported ``` ::: ## Multiple Head Instances It's possible to have multiple instances of the `` component throughout your application. For example, your layout can set some default `` elements, and then your individual pages can override those defaults. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx // Layout.jsx import { Head } from '@inertiajs/react' export default () => ( My app ) // About.jsx import { Head } from '@inertiajs/react' export default () => ( About - My app ) ``` \== Svelte ```js // Not supported ``` ::: Inertia will only ever render one `` tag; however, all other tags will be stacked since it's valid to have multiple instances of them. To avoid duplicate tags in your `<head>`, you can use the `head-key` property, which will make sure the tag is only rendered once. This is illustrated in the example above for the `<meta name="description">` tag. The code example above will render the following HTML. ```html <head> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <title>About - My app ``` ## Head Extension When building a real application, it can sometimes be helpful to create a custom head component that extends Inertia's `` component. This gives you a place to set app-wide defaults, such as appending the app name to the page title. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx // AppHead.js import { Head } from '@inertiajs/react' export default ({ title, children }) => { return ( {title ? `${title} - My App` : 'My App'} {children} ) } ``` \== Svelte ```js // Not supported ``` ::: Once you have created the custom component, you can just start using it in your pages. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import AppHead from './AppHead' export default () => ``` \== Svelte ```js // Not supported ``` ::: ## Inertia Attribute on Elements @available\_since core=2.2.13 Inertia has historically used the `inertia` attribute to track and manage elements in the document ``. However, you can now opt-in to using the more standards-compliant `data-inertia` attribute instead. According to the HTML specification, custom attributes should be prefixed with `data-` to avoid conflicts with future HTML standards. To enable this for `@inertiajs/core` < 3.0, configure the `future.useDataInertiaHeadAttribute` option in your [application defaults](/guide/client-side-setup#configuring-defaults). ```js createInertiaApp({ // resolve, setup, etc. defaults: { future: { useDataInertiaHeadAttribute: true, }, }, }) ``` --- --- url: /guide/typescript.md --- # TypeScript Inertia provides first-class TypeScript support. You may configure global types using declaration merging, and pass generics to hooks and router methods for type-safe props, forms, and state management. ## Using pnpm Due to pnpm's strict dependency isolation, `@inertiajs/core` is not accessible at `node_modules/@inertiajs/core`. Instead, it's nested inside `.pnpm/`, which prevents TypeScript module augmentation from resolving the module. You may fix this by configuring pnpm to [hoist the package](https://pnpm.io/settings#publichoistpattern). Add the following to your `.npmrc` file and run `pnpm install`. ```ini public-hoist-pattern[]=@inertiajs/core ``` Alternatively, you may add `@inertiajs/core` as a direct dependency in your project. ```bash pnpm add @inertiajs/core ``` ## Global Configuration You may configure Inertia's types globally by augmenting the `InertiaConfig` interface in the `@inertiajs/core` module. This is typically done in a `global.d.ts` file in your project's root or `types` directory. ```ts // global.d.ts import '@inertiajs/core' declare module '@inertiajs/core' { export interface InertiaConfig { sharedPageProps: { auth: { user: { id: number; name: string } | null } appName: string } flashDataType: { toast?: { type: 'success' | 'error'; message: string } } errorValueType: string[] layoutProps: { title: string showSidebar: boolean } namedLayoutProps: { app: { title: string; theme: 'light' | 'dark' } content: { padding: string; maxWidth: string } } } } ``` > \[!NOTE] > The `import` statement (or `export {}`) is required to make this file a module. Without it, `declare module` replaces the module definition instead of augmenting it. Your `tsconfig.json` also needs to include `.d.ts` files, so make sure a pattern like `"@/**/*.d.ts"` is present in the `include` array. ### Shared Page Props The `sharedPageProps` option defines the type of data that is [shared](/guide/shared-data) with every page in your application. With this configuration, `page.props.auth` and `page.props.appName` will be properly typed everywhere. ```ts sharedPageProps: { auth: { user: { id: number; name: string } | null } appName: string } ``` ### Flash Data The `flashDataType` option defines the type of [flash data](/guide/flash-data) in your application. ```ts flashDataType: { toast?: { type: 'success' | 'error'; message: string } } ``` ### Error Values By default, validation error values are typed as `string`. You may configure TypeScript to expect arrays instead for Rails' default (with `model.errors`) — multiple errors per field. ```ts errorValueType: string[] ``` ### Layout Props The `layoutProps` option types the data accepted by `setLayoutProps()`. The `namedLayoutProps` option types the data accepted by `setLayoutProps('name', props)`, keyed by layout name. ```ts layoutProps: { title: string showSidebar: boolean } namedLayoutProps: { app: { title: string theme: 'light' | 'dark' } content: { padding: string maxWidth: string } } ``` With this configuration, `setLayoutProps({ title: 'Dashboard' })` is type-checked, and `setLayoutProps('app', { theme: 'dark' })` validates both the layout name and its props. You may also pass a generic type parameter directly to `setLayoutProps` for ad-hoc typing without configuring the global `InertiaConfig` interface. ```ts setLayoutProps<{ custom: string }>({ custom: 'value' }) setLayoutProps<{ collapsed: boolean }>('sidebar', { collapsed: true }) ``` ## Page Components You may type the `import.meta.glob` result for better type safety when resolving page components. :::tabs key:frameworks \== Vue ```ts import { createInertiaApp } from '@inertiajs/vue3' import type { DefineComponent } from 'vue' createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('../pages/**/*.vue') return pages[`../pages/${name}.vue`]() }, // ... }) ``` \== React ```tsx import { createInertiaApp, type ResolvedComponent } from '@inertiajs/react' createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('../pages/**/*.tsx') return pages[`../pages/${name}.tsx`]() }, // ... }) ``` \== Svelte ```ts import { createInertiaApp, type ResolvedComponent } from '@inertiajs/svelte' createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('../pages/**/*.svelte') return pages[`../pages/${name}.svelte`]() }, // ... }) ``` ::: ## Page Props You may type page-specific props by passing a generic to `usePage()`. These are merged with your global `sharedPageProps`, giving you autocomplete and type checking for both shared and page-specific data. :::tabs key:frameworks \== Vue ```vue ``` \== React ```tsx import { usePage } from '@inertiajs/react' export default function Posts() { const page = usePage<{ posts: { id: number; title: string }[] }>() return (
      {page.props.posts.map((post) => (
    • {post.title}
    • ))}
    ) } ``` \== Svelte ```svelte ``` ::: ## Form Helper The [form helper](/guide/forms#form-helper) accepts a generic type parameter for type-safe form data and error handling. This provides autocomplete for form fields and errors, and prevents typos in field names. :::tabs key:frameworks \== Vue ```vue ``` \== React ```tsx import { useForm } from '@inertiajs/react' export default function CreateUser() { const form = useForm<{ name: string email: string company: { name: string } }>({ name: '', email: '', company: { name: '' }, }) return null } ``` \== Svelte ```svelte ``` ::: ### Nested Data and Arrays Form types fully support nested objects and arrays. You may access and update nested fields using dot notation, and error keys are automatically typed to match. ```ts import { useForm } from '@inertiajs/react' const form = useForm<{ user: { name: string; email: string } tags: { id: number; label: string }[] }>({ user: { name: '', email: '' }, tags: [], }) ``` ## Form Component @available\_since core=3.0.0 The `
    ` component accepts a generic type parameter for type-safe slot props. In React, you may pass the generic directly. In Vue and Svelte, you may use the `createForm` helper to create a typed form component. :::tabs key:frameworks \== Vue ```vue ``` \== React ```tsx import { Form } from '@inertiajs/react' interface UserForm { name: string email: string } export default function CreateUser() { return ( action="/users" method="post"> {({ errors }) => ( <> {errors.name &&
    {errors.name}
    } )} ) } ``` \== Svelte ```svelte {#snippet children({ errors })} {#if errors.name}
    {errors.name}
    {/if} {/snippet}
    ``` ::: The generic provides autocomplete and type checking for the `errors` object, `setError`, `clearErrors`, and other slot props that reference form fields. ### useFormContext @available\_since core=3.0.0 The `useFormContext()` function also accepts a generic type parameter, providing type-safe access to the form context from child components. :::tabs key:frameworks \== React ```tsx import { useFormContext } from '@inertiajs/react' const form = useFormContext() ``` \== Vue ```vue ``` \== Svelte ```svelte ``` ::: ## HTTP Helper The [`useHttp`](/guide/http-requests) hook accepts two generic type parameters: the form data type and an optional default response type. ```ts import { useHttp } from '@inertiajs/react' interface UserForm { name: string email: string } interface UserResponse { id: number name: string } const http = useHttp({ name: '', email: '' }) ``` ### Per-Request Response Types Each HTTP method accepts its own generic type parameter, allowing you to override the response type on a per-call basis. This is useful when different endpoints return different response shapes. ```ts interface OrderResponse { orderId: string total: number } // Override the response type per request... const user: UserResponse = await http.post('/api/users') const order: OrderResponse = await http.get('/api/orders/123') const submitted: UserResponse = await http.submit( 'post', '/api/users', ) // The onSuccess callback is also typed... await http.post('/api/users', { onSuccess: (response) => { console.log(response.id, response.name) }, }) ``` ## Remembering State The `useRemember` hook accepts a generic type parameter for type-safe local state persistence, providing autocomplete and ensuring values match the expected types. :::tabs key:frameworks \== Vue ```vue ``` \== React ```tsx import { useRemember } from '@inertiajs/react' export default function Users() { const [filters, setFilters] = useRemember<{ search: string status: 'active' | 'inactive' | 'all' }>({ search: '', status: 'all', }) return null } ``` \== Svelte ```svelte ``` ::: ## Restoring State The `router.restore()` method accepts a generic for typing state restored from [history](/guide/remembering-state#manually-saving-state). ```ts import { router } from '@inertiajs/react' interface TableState { sortBy: string sortDesc: boolean page: number } const restored = router.restore('table-state') if (restored) { console.log(restored.sortBy) } ``` ## Router Requests Router methods accept a generic for typing request data, providing type checking for the data being sent. ```ts import { router } from '@inertiajs/react' interface CreateUserData { name: string email: string } router.post('/users', { name: 'John', email: 'john@example.com', }) ``` ## Scoped Flash Data The `router.flash()` method accepts a generic for typing page or section-specific flash data, separate from the global `flashDataType` configuration. ```ts import { router } from '@inertiajs/react' router.flash<{ paymentError: string }>({ paymentError: 'Card declined' }) ``` ## Client-Side Visits The `router.push()` and `router.replace()` methods accept a generic for typing [client-side visit](/guide/manual-visits#client-side-visits) props. ```ts import { router } from '@inertiajs/react' interface UserPageProps { user: { id: number; name: string } } router.push({ component: 'Users/Show', url: '/users/1', props: { user: { id: 1, name: 'John' } }, }) router.replace({ props: (current) => ({ ...current, user: { ...current.user, name: 'Updated' }, }), }) ``` --- --- url: /guide/upgrade-guide.md --- # Upgrade Guide for v3.0 ## What's New Inertia.js v3.0 is a major release focused on simplicity and developer experience. Axios has been replaced with a built-in XHR client for a smaller bundle, SSR now works out of the box during development without a separate Node.js server, and the new `@inertiajs/vite` plugin handles page resolution and SSR configuration automatically. This release also introduces standalone HTTP requests via the `useHttp` hook, optimistic updates with automatic rollback, layout props for sharing data between pages and layouts, and improved exception handling. * [Vite Plugin](/guide/client-side-setup#installation) — Automatic page resolution, SSR setup, and optional setup/resolve callbacks. * [HTTP Requests](/guide/http-requests) — Make standalone HTTP requests with the useHttp hook, without triggering page visits. * [Optimistic Updates](/guide/optimistic-updates) — Apply data changes instantly before the server responds, with automatic rollback on failure. * [Layout Props](/guide/layouts#layout-props) — Share dynamic data between pages and persistent layouts with the useLayoutProps hook. * [Simplified SSR](/guide/server-side-rendering) — SSR works automatically in Vite dev mode. No separate Node.js server needed. * [Exception Handling](/guide/error-handling#production) — Render custom Inertia error pages directly from your exception handler, with shared data. This release also includes several additional improvements: * [Instant visits](/guide/instant-visits) that swap to the target component before the server responds * [Form component generics](/guide/typescript#form-component) for type-safe errors and slot props * [Disable SSR per-route](/guide/server-side-rendering#disabling-ssr) via middleware or facade * [Improved SSR error messages](/guide/server-side-rendering#error-handling) with component names, URLs, and actionable hints * [Page object in resolve callback](/guide/client-side-setup#manual-setup) for context-aware component resolution * [Built-in HTTP interceptors](/guide/client-side-setup#interceptors) without Axios * [Default layout](/guide/layouts#default-layouts) option in `createInertiaApp` * [`preserveErrors`](/guide/partial-reloads#preserving-errors) option to preserve validation errors during partial reloads ## Upgrade Dependencies To upgrade to Inertia.js v3.0, first use npm to install the client-side adapter of your choice: :::tabs key:frameworks \== Vue ```bash npm install @inertiajs/vue3@^3.0 ``` \== React ```bash npm install @inertiajs/react@^3.0 ``` \== Svelte ```bash npm install @inertiajs/svelte@^3.0 ``` ::: You may also install the new optional Vite plugin, which provides a simplified SSR setup and a `pages` shorthand for component resolution: ```bash npm install @inertiajs/vite@^3.0 ``` Next, upgrade the `inertia_rails` gem: ```ruby gem 'inertia_rails', '~> 3.19' ``` Make sure you are using configuration options that are compatible with Inertia.js v3.0. ```ruby # config/initializers/inertia.rb InertiaRails.configure do |config| config.use_script_element_for_initial_page = true config.use_data_inertia_head_attribute = true config.always_include_errors_hash = true # ... end ``` ## Breaking Changes *** ### Requirements #### React 19+ The React adapter now requires React 19. React 18 and below are no longer supported. #### Svelte 5+ The Svelte adapter now requires Svelte 5. Svelte 4 and below are no longer supported. All Svelte code should be updated to use the Svelte 5 runes syntax (`$props()`, `$state()`, `$effect()`, etc). ### Axios Removed Inertia no longer ships with or requires Axios. For most applications, this requires no changes. The built-in XHR client supports [interceptors](/guide/client-side-setup#interceptors) as well, so Axios interceptors may be migrated directly. You may also continue using Axios via the [Axios adapter](/guide/client-side-setup#using-axios), or provide a fully [custom HTTP client](/guide/client-side-setup#custom-http-client). ### `qs` Dependency Removed The `qs` package has been replaced with a built-in query string implementation and is no longer included as a dependency of `@inertiajs/core`. Inertia's internal query string handling remains the same, but you should install `qs` directly if your application imports it. ```bash npm install qs ``` ### `lodash-es` Dependency Removed The `lodash-es` package has been replaced with `es-toolkit` and is no longer included as a dependency of `@inertiajs/core`. You should install `lodash-es` directly if your application imports it. ```bash npm install lodash-es ``` ### Event Renames Two global events have been renamed for clarity: | v2 Name | v3 Name | Document Event | | ----------- | --------------- | ----------------------- | | `invalid` | `httpException` | `inertia:httpException` | | `exception` | `networkError` | `inertia:networkError` | Global event listeners should be updated accordingly: ```js // Before (v2) router.on('invalid', (event) => { ... }) router.on('exception', (event) => { ... }) // After (v3) router.on('httpException', (event) => { ... }) router.on('networkError', (event) => { ... }) ``` You may also handle these events per-visit using the new `onHttpException` and `onNetworkError` callbacks: ```js router.post('/users', data, { onHttpException: (response) => { ... }, onNetworkError: (error) => { ... }, }) ``` ### `router.cancel()` Replaced The `router.cancel()` method has been replaced by `router.cancelAll()`. In v2, `cancel()` only cancelled synchronous requests. The new `cancelAll()` method cancels all synchronous, asynchronous, and prefetch requests by default. You may pass options to limit which request types are cancelled. ```js // Before (v2) — only cancelled sync requests router.cancel() // After (v3) — cancels all request types router.cancelAll() // To match v2 behavior (sync only)... router.cancelAll({ async: false, prefetch: false }) ``` See the [visit cancellation](/guide/manual-visits#visit-cancellation) documentation for more details. ### Future Options Removed The `future` configuration namespace has been removed. All four future options from v2 are now always enabled and no longer configurable: * `future.preserveEqualProps` * `future.useDataInertiaHeadAttribute` * `future.useDialogForErrorModal` * `future.useScriptElementForInitialPage` ```js // Before (v2) createInertiaApp({ defaults: { future: { preserveEqualProps: true, useDataInertiaHeadAttribute: true, useDialogForErrorModal: true, useScriptElementForInitialPage: true, }, }, }) // After (v3) - just remove the `future` block createInertiaApp({ // ... }) ``` Initial page data is now always passed via a ` ``` \== React ```jsx import { useState } from 'react' import { router, usePage } from '@inertiajs/react' export default function Edit() { const { errors } = usePage().props const [values, setValues] = useState({ first_name: null, last_name: null, email: null, }) function handleChange(e) { setValues((values) => ({ ...values, [e.target.id]: e.target.value, })) } function handleSubmit(e) { e.preventDefault() router.post('/users', values) } return (
    {errors.first_name &&
    {errors.first_name}
    } {errors.last_name &&
    {errors.last_name}
    } {errors.email &&
    {errors.email}
    }
    ) } ``` \== Svelte ```svelte
    { e.preventDefault() handleSubmit() }} > {#if errors.first_name}
    {errors.first_name}
    {/if} {#if errors.last_name}
    {errors.last_name}
    {/if} {#if errors.email}
    {errors.email}
    {/if}
    ``` ::: When using the Vue adapters, you may also access the errors via the `page.props.errors` object. ## Repopulating Input While handling errors in Inertia is similar to full page form submissions, Inertia offers even more benefits. In fact, you don't even need to manually repopulate old form input data. When validation errors occur, the user is typically redirected back to the form page they were previously on. And, by default, Inertia automatically preserves the [component state](/guide/manual-visits#state-preservation) for `post`, `put`, `patch`, and `delete` requests. Therefore, all the old form input data remains exactly as it was when the user submitted the form. So, the only work remaining is to display any validation errors using the `errors` prop. ## Error Bags > \[!NOTE] > Error bags are a Laravel-specific feature that relies on Laravel's `ViewErrorBag` system. In Rails, this feature is typically unnecessary because: > > * Forms submit to separate controller actions, so only one set of errors is returned per request > * The [form helper](/guide/forms.md) automatically scopes validation errors to each form instance > > If you have multiple forms on a page, use the `useForm()` helper and each form will maintain its own isolated error state. --- --- url: /guide/view-transitions.md --- # View Transitions @available\_since core=2.2.13 Inertia supports the [View Transitions API](https://developer.chrome.com/docs/web-platform/view-transitions), allowing you to animate page transitions. > \[!NOTE] > The View Transitions API is a [relatively new browser feature](https://caniuse.com/view-transitions). Inertia gracefully falls back to standard page transitions in browsers that don't support the API. ## Enabling Transitions You may enable view transitions for a visit by setting the `viewTransition` option to `true`. By default, this will apply a cross-fade transition between pages. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.visit('/another-page', { viewTransition: true }) ``` \== React ```js import { router } from '@inertiajs/react' router.visit('/another-page', { viewTransition: true }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.visit('/another-page', { viewTransition: true }) ``` ::: ## Transition Callbacks You may also pass a callback to the `viewTransition` option, which will receive the standard [`ViewTransition`](https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition) instance provided by the browser. This allows you to hook into the various promises provided by the API. :::tabs key:frameworks \== Vue ```js import { router } from '@inertiajs/vue3' router.visit('/another-page', { viewTransition: (transition) => { transition.ready.then(() => console.log('Transition ready')) transition.updateCallbackDone.then(() => console.log('DOM updated')) transition.finished.then(() => console.log('Transition finished')) }, }) ``` \== React ```js import { router } from '@inertiajs/react' router.visit('/another-page', { viewTransition: (transition) => { transition.ready.then(() => console.log('Transition ready')) transition.updateCallbackDone.then(() => console.log('DOM updated')) transition.finished.then(() => console.log('Transition finished')) }, }) ``` \== Svelte ```js import { router } from '@inertiajs/svelte' router.visit('/another-page', { viewTransition: (transition) => { transition.ready.then(() => console.log('Transition ready')) transition.updateCallbackDone.then(() => console.log('DOM updated')) transition.finished.then(() => console.log('Transition finished')) }, }) ``` ::: ## Links The `viewTransition` option is also available on the `Link` component. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { router } from '@inertiajs/react' export default () => ( Navigate ) ``` \== Svelte ```svelte Navigate ``` ::: You may also pass a callback to access the `ViewTransition` instance. :::tabs key:frameworks \== Vue ```vue ``` \== React ```jsx import { router } from '@inertiajs/react' export default () => ( transition.finished.then(() => console.log('Done')) } > Navigate ) ``` \== Svelte ```svelte transition.finished.then(() => console.log('Done'))} > Navigate ``` ::: ## Global Configuration You may enable view transitions globally for all visits by configuring the `visitOptions` callback when [initializing your Inertia app](/guide/client-side-setup#configuring-defaults). :::tabs key:frameworks \== Vue ```js import { createInertiaApp } from '@inertiajs/vue3' createInertiaApp({ // ... defaults: { visitOptions: (href, options) => { return { viewTransition: true } }, }, }) ``` \== React ```js import { createInertiaApp } from '@inertiajs/react' createInertiaApp({ // ... defaults: { visitOptions: (href, options) => { return { viewTransition: true } }, }, }) ``` \== Svelte ```js import { createInertiaApp } from '@inertiajs/svelte' createInertiaApp({ // ... defaults: { visitOptions: (href, options) => { return { viewTransition: true } }, }, }) ``` ::: ## Customizing Transitions You may customize the transition animations using CSS. The View Transitions API uses several pseudo-elements that you can target with CSS to create custom animations. The following examples are taken from the [Chrome documentation](https://developer.chrome.com/docs/web-platform/view-transitions/same-document#customize_the_transition). ```css @keyframes fade-in { from { opacity: 0; } } @keyframes fade-out { to { opacity: 0; } } @keyframes slide-from-right { from { transform: translateX(30px); } } @keyframes slide-to-left { to { transform: translateX(-30px); } } ::view-transition-old(root) { animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out, 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; } ::view-transition-new(root) { animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; } ``` You may also animate individual elements between pages by assigning them a unique `view-transition-name`. For example, you may animate an avatar from a large size on a profile page to a small size on a dashboard. :::tabs key:frameworks \== Vue ```vue ``` ```vue ``` \== React ```jsx // Profile.jsx export default function Profile() { return User } ``` ```jsx // Dashboard.jsx export default function Dashboard() { return User } ``` ```css .avatar-large { view-transition-name: user-avatar; width: auto; height: 200px; } .avatar-small { view-transition-name: user-avatar; width: auto; height: 40px; } ``` \== Svelte ```svelte User ``` ```svelte User ``` ::: You may customize view transitions to your liking using any CSS animations you wish. For more information, please consult the [View Transitions API documentation](https://developer.chrome.com/docs/web-platform/view-transitions/same-document#customize_the_transition). --- --- url: /guide/who-is-it-for.md --- # Who Is Inertia.js For? Inertia was crafted for development teams and solo hackers who typically build server-side rendered applications using frameworks like Laravel, Ruby on Rails, Django, or Phoenix. You're used to creating controllers, retrieving data from the database (via an ORM), and rendering views. But what happens when you want to replace your server-side rendered views with a modern, JavaScript-based single-page application frontend? The answer is always "you need to build an API". Because that's how modern SPAs are built. This means building a REST or GraphQL API. It means figuring out authentication and authorization for that API. It means client-side state management. It means setting up a new Git repository. It means a more complicated deployment strategy. And this list goes on. It's a complete paradigm shift, and often a complete mess. We think there is a better way. > **Inertia empowers you to build a modern, JavaScript-based single-page application without the tiresome complexity.** Inertia works just like a classic server-side rendered application. You create controllers, you get data from the database (via your ORM), and you render views. But, Inertia views are JavaScript page components written in React, Vue, or Svelte. This means you get all the power of a client-side application and modern SPA experience, but you don't need to build an API. We think it's a breath of fresh air that will supercharge your productivity.