inertia-rails-controllers
ALWAYS `render inertia: { key: data }` to pass data as props — instance variables are NOT auto-passed (only alba-inertia does that). Rails controller patterns for Inertia.js: render inertia, prop types (defer, optional, merge, scroll), shared data, flash, PRG redirects, validation errors. Use when w
By inertia-rails · 449 installs
npx skills add inertia-rails/skills --skill inertia-rails-controllers
Source repository · Upstream listing
Inertia Rails Controllers
Server side patterns for Rails controllers serving Inertia responses.
Before adding a prop, ask:
Needed on every page? → inertia share in a base controller ( InertiaController ), not a per action prop
Expensive to compute? → InertiaRails.defer — page loads fast, data streams in after
Only needed on partial reload? → InertiaRails.optional — skipped on initial load
Reference data that rarely changes? → InertiaRails.once — cached across navigations
NEVER:
Use redirect to for external URLs (Stripe, OAuth, SSO) — it returns 302 but the Inertia client tries to parse the response as JSON, causing a broken redirect. Use inertia location (returns 409 + X Inertia Location header).
Use errors.full messages for validation errors — it produces flat strings without field keys, so errors can't be mapped to the corresponding input fields on the frontend. Use errors.to hash(true) .
Use inertia.defer , Inertia.defer , or inertia rails.defer — the correct syntax is InertiaRails.defer { ... } . All prop helpers are module methods on the InertiaRails constant.
Assume instance variables are auto passed as props — they are NOT (unless alba inertia gem is configured). Every action that passes props to the frontend MUST call render inertia: { key: data } .
Use success / error as flash keys without updating config.flash keys — Rails defaults to notice / alert . Custom keys must be added to both the initializer config and the FlashData TypeScript type.
Render Syntax
default render: true TRAP: This setting only auto infers the component
name from controller/action — it does NOT auto pass instance variables as
props. Writing @posts = Post.all in an action with default render: true
renders the correct component but sends zero data to the frontend.
Instance variables are only auto serialized as props when alba inertia gem
is configured — check Gemfile before relying on this. Without it, you MUST
use render inertia: { posts: data } to pass any data to the page.
Empty actions ( def index; end ) are correct ONLY for pages that need no data
(e.g., a static dashboard page, a login form). If the action queries the
database, it MUST call render inertia: with data.
Situation Syntax Component path
Action loads data render inertia: { users: data } Inferred from controller/action
Action loads NO data (static page) Empty action or render inertia: {} Inferred from controller/action
Rendering a different page render inertia: 'errors/show', props: { error: e } Explicit path
Rule of thumb: If your action touches the database, it MUST call render inertia: with data.
If the action body is empty, the page receives only shared props (from inertia share ).
Note: If the project uses the alba inertia gem (check Gemfile ), instance
variables are auto serialized as props and explicit render inertia: is not needed.
See the alba inertia skill for that convention.
Prop Types
InertiaRails.defer — NOT inertia.defer , NOT Inertia.defer . All prop helpers are module methods on InertiaRails .
Type Syntax Behavior
Regular { key: value } Always evaluated, always included
Lazy { expensive value } Included on initial page render, lazily evaluated on partial reloads
Optional InertiaRails.optional { ... } Only evaluated on partial reload requesting it
Defer InertiaRails.defer { ... } Loaded after initial page render
Defer (grouped) InertiaRails.defer(group: 'name') { ... } Grouped deferred — fetched in parallel
Once InertiaRails.once { ... } Resolved once, remembered across navigations
Merge InertiaRails.merge { ... } Appended to existing array (infinite scroll)
Deep merge InertiaRails.deep merge { ... } Deep merged into existing object
Always InertiaRails.always { ... } Included even in partial reloads
Scroll InertiaRails.scroll { ... } Scroll aware prop for infinite scroll
Deferred Props — Full Stack Example
Server defers slow data, client shows fallback then swaps in content:
Shared Data
Use inertia share in controllers — it needs controller context ( current user ,
request). The initializer only handles config. settings (version, flash keys).
Lambda and action scoped variants are in [ references/configuration.md ](references/configuration.md).
Evaluation order: Multiple inertia share calls merge top down. If a child
controller shares the same key as a parent, the child's value wins. Block and lambda
shares are lazily evaluated per request — they don't run for non Inertia requests.
Flash Messages
Flash is automatic. Configure exposed keys if needed:
Use standard Rails flash in controllers:
Redirects & Validation Errors
After create/update/delete, always redirect (Post Redirect Get). Standard Rails
redirect to works. The Inertia specific part is validation error handling:
to hash vs to hash(true) : to hash gives { name: ["can't be blank"] } ,
to hash(true) gives { name: ["Name can't be blank"] } . Keys must match input
name attributes — mismatched keys mean errors won't display next to the right field.
NEVER use errors.full messages — it produces flat strings without field keys,
so errors can't be mapped to the corresponding input fields on the frontend.
Authorization as Props
Pass permissions as per resource can hash — frontend controls visibility,
server enforces access. See inertia rails controllers + inertia rails pages skills.
MANDATORY — READ ENTIRE FILE when implementing authorization props:
[ references/authorization.md ](references/authorization.md) (~40 lines) — full stack
can pattern with Action Policy/Pundit/CanCanCan examples.
Do NOT load if not passing permission data to the frontend.
External Redirects ( inertia location )
CRITICAL: redirect to for external URLs breaks Inertia — the client
receives a 302 but tries to handle it as an Inertia response (JSON), not a
full page redirect. inertia location returns 409 with X Inertia Location
header, which tells the client to do window.location = url .
Use inertia location for any URL outside the Inertia app: payment
providers, OAuth, external services.
History Encryption
Encrypts page data in browser history state — config.encrypt history = Rails.env.production? .
Use redirect to path, inertia: { clear history: true } on logout/role change.
Full setup with server side and client side examples is in
[ references/configuration.md ](references/configuration.md).
Configuration
See [ references/configuration.md ](references/configuration.md) for all
InertiaRails.configure options (version, encrypt history, flash keys, etc.).
Troubleshooting
Symptom Cause Fix
302 loop on Stripe/OAuth redirect redirect to for external URL Use inertia location — it returns 409 + X Inertia Location header
Errors don't display next to fields Error keys don't match input name to hash keys must match input name attributes exactly
TS2305: postsPath not found in @/routes js routes not regenerated after adding routes Run rails js routes:generate after changing config/routes.rb
Related Skills
Form error display → inertia rails forms
Flash toast UI → inertia rails pages (access) + shadcn inertia (Sonner)
Deferred on client → inertia rails pages ( <Deferred component)
Type safe props → inertia rails typescript or alba inertia (serializers)
Testing → inertia rails testing
References
MANDATORY — READ ENTIRE FILE when using advanced prop types ( merge ,
scroll , deep merge ) or combining multiple prop options:
[ references/prop types.md ](references/prop types.md) (~180 lines) — detailed behavior,
edge cases, and combination rules for all prop types.
Do NOT load prop types.md for basic defer , optional , once , or always
usage — the table above is sufficient.
Load [ references/configuration.md ](references/configuration.md) (~180 lines) only when
setting up InertiaRails.configure for the first time or debugging configuration
issues. Do NOT load for routine controller work.