inertia-rails-controllers

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Inertia Rails Controllers

Inertia Rails 控制器

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.
为返回Inertia响应的Rails控制器提供服务端开发模式。
添加prop前请先确认:
  • 是否每个页面都需要? → 在基础控制器(
    InertiaController
    )中使用
    inertia_share
    ,不要作为单个动作的prop
  • 计算成本高吗? → 使用
    InertiaRails.defer
    ——页面快速加载,数据后续流式传输
  • 仅在局部重载时需要? → 使用
    InertiaRails.optional
    ——初始加载时会跳过
  • 引用极少变更的参考数据? → 使用
    InertiaRails.once
    ——在导航间缓存数据
绝对禁止:
  • 对外部URL(Stripe、OAuth、SSO)使用
    redirect_to
    ——它返回302状态码,但Inertia客户端会尝试将响应解析为JSON,导致重定向失败。请使用
    inertia_location
    (返回409状态码 +
    X-Inertia-Location
    请求头)。
  • 对验证错误使用
    errors.full_messages
    ——它会生成无字段键的扁平字符串,导致前端无法将错误映射到对应的输入字段。请使用
    errors.to_hash(true)
  • 使用
    inertia.defer
    Inertia.defer
    inertia_rails.defer
    ——正确语法是
    InertiaRails.defer { ... }
    。所有prop助手都是
    InertiaRails
    常量的模块方法。
  • 假设实例变量会自动作为prop传递——事实并非如此(除非配置了
    alba-inertia
    gem)。每个向前端传递prop的动作都必须调用
    render inertia: { key: data }
  • 在未更新
    config.flash_keys
    的情况下使用
    success
    /
    error
    作为flash键——Rails默认使用
    notice
    /
    alert
    。自定义键必须同时添加到初始化器配置和
    FlashData
    TypeScript类型中。

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.
SituationSyntaxComponent 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
).
ruby
undefined
default_render: true
陷阱:
此设置仅会根据控制器/动作自动推断组件名称——不会自动传递实例变量作为prop。在开启
default_render: true
的动作中编写
@posts = Post.all
会渲染正确的组件,但不会向前端发送任何数据。 只有在配置了
alba-inertia
gem时,实例变量才会自动序列化为prop——使用前请检查
Gemfile
。如果没有配置该gem,必须使用
render inertia: { posts: data }
才能向页面传递数据。
空动作(
def index; end
)仅适用于不需要数据的页面(例如静态仪表盘页面、登录表单)。如果动作需要查询数据库,则必须调用带数据的
render inertia:
场景语法组件路径
动作加载数据
render inertia: { users: data }
根据控制器/动作自动推断
动作不加载数据(静态页面)空动作或
render inertia: {}
根据控制器/动作自动推断
渲染其他页面
render inertia: 'errors/show', props: { error: e }
显式指定路径
经验法则: 如果你的动作需要查询数据库,就必须调用带数据的
render inertia:
。 如果动作体为空,页面只会接收来自
inertia_share
的共享prop。
ruby
undefined

CORRECT — data passed as props

正确写法——数据作为prop传递

def index render inertia: { users: users_data, stats: InertiaRails.defer { ExpensiveQuery.run } } end
def index render inertia: { users: users_data, stats: InertiaRails.defer { ExpensiveQuery.run } } end

CORRECT — static page, no data needed

正确写法——静态页面,无需数据

def index; end
def index; end

WRONG — @posts is NEVER sent to the frontend (without alba-inertia)

错误写法——@posts永远不会发送到前端(未配置alba-inertia时)

def index @posts = Post.all end

> **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.
def index @posts = Post.all end

> 注意:如果项目使用了`alba-inertia` gem(请检查`Gemfile`),实例变量会自动序列化为prop,无需显式调用`render inertia:`。
> 相关约定请查看`alba-inertia`技能文档。

Prop Types

Prop 类型

InertiaRails.defer
— NOT
inertia.defer
, NOT
Inertia.defer
. All prop helpers are module methods on
InertiaRails
.
TypeSyntaxBehavior
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
ruby
def index
  render inertia: {
    filters: filter_params,
    messages: -> { messages_scope.as_json },
    stats: InertiaRails.defer { Dashboard.stats },
    chart: InertiaRails.defer(group: 'analytics') { Dashboard.chart },
    countries: InertiaRails.once { Country.pluck(:name, :code) },
    posts: InertiaRails.merge { @posts.as_json },
    csrf: InertiaRails.always { form_authenticity_token },
  }
end
InertiaRails.defer
—— 不要使用
inertia.defer
Inertia.defer
。所有prop助手都是
InertiaRails
的模块方法。
类型语法行为
常规
{ key: value }
始终执行,始终包含
延迟加载
-> { expensive_value }
初始页面渲染时包含,局部重载时延迟执行
可选
InertiaRails.optional { ... }
仅在请求该prop的局部重载时执行
延迟传递
InertiaRails.defer { ... }
初始页面渲染完成后加载
分组延迟传递
InertiaRails.defer(group: 'name') { ... }
分组延迟——并行获取数据
单次加载
InertiaRails.once { ... }
仅解析一次,在导航间缓存
合并
InertiaRails.merge { ... }
追加到现有数组(无限滚动场景)
深度合并
InertiaRails.deep_merge { ... }
深度合并到现有对象
始终包含
InertiaRails.always { ... }
即使在局部重载中也会包含
滚动感知
InertiaRails.scroll { ... }
适用于无限滚动的滚动感知prop
ruby
def index
  render inertia: {
    filters: filter_params,
    messages: -> { messages_scope.as_json },
    stats: InertiaRails.defer { Dashboard.stats },
    chart: InertiaRails.defer(group: 'analytics') { Dashboard.chart },
    countries: InertiaRails.once { Country.pluck(:name, :code) },
    posts: InertiaRails.merge { @posts.as_json },
    csrf: InertiaRails.always { form_authenticity_token },
  }
end

Deferred Props — Full Stack Example

延迟Prop——全栈示例

Server defers slow data, client shows fallback then swaps in content:
ruby
undefined
服务端延迟加载慢数据,客户端先显示占位内容,之后替换为真实内容:
ruby
undefined

Controller

控制器

def show render inertia: { basic_stats: Stats.quick_summary, analytics: InertiaRails.defer { Analytics.compute_slow }, } end

```tsx
// Page component — child reads deferred prop from page props
import { Deferred, usePage } from '@inertiajs/react'

export default function Dashboard({ basic_stats }: Props) {
  return (
    <>
      <QuickStats data={basic_stats} />
      <Deferred data="analytics" fallback={<div>Loading analytics...</div>}>
        <AnalyticsPanel />
      </Deferred>
    </>
  )
}

function AnalyticsPanel() {
  const { analytics } = usePage<{ analytics: Analytics }>().props
  return <div>{analytics.revenue}</div>
}
def show render inertia: { basic_stats: Stats.quick_summary, analytics: InertiaRails.defer { Analytics.compute_slow }, } end

```tsx
// 页面组件——子组件从页面props中读取延迟prop
import { Deferred, usePage } from '@inertiajs/react'

export default function Dashboard({ basic_stats }: Props) {
  return (
    <>
      <QuickStats data={basic_stats} />
      <Deferred data="analytics" fallback={<div>正在加载分析数据...</div>}>
        <AnalyticsPanel />
      </Deferred>
    </>
  )
}

function AnalyticsPanel() {
  const { analytics } = usePage<{ analytics: Analytics }>().props
  return <div>{analytics.revenue}</div>
}

Shared Data

共享数据

Use
inertia_share
in controllers — it needs controller context (
current_user
, request). The initializer only handles
config.*
settings (version, flash_keys).
ruby
class ApplicationController < ActionController::Base
  # Static
  inertia_share app_name: 'MyApp'

  # Using lambdas (most common)
  inertia_share auth: -> { { user: current_user&.as_json(only: [:id, :name, :email, :role]) } }

  # Conditional
  inertia_share if: :user_signed_in? do
    { notifications: -> { current_user.unread_notifications_count } }
  end
end
Lambda and action-scoped variants are in
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.
在控制器中使用
inertia_share
——它需要控制器上下文(如
current_user
、request)。初始化器仅处理
config.*
设置(如版本、flash_keys)。
ruby
class ApplicationController < ActionController::Base
  # 静态数据
  inertia_share app_name: 'MyApp'

  # 使用lambda(最常用方式)
  inertia_share auth: -> { { user: current_user&.as_json(only: [:id, :name, :email, :role]) } }

  # 条件式共享
  inertia_share if: :user_signed_in? do
    { notifications: -> { current_user.unread_notifications_count } }
  end
end
Lambda和动作作用域的变体请查看
references/configuration.md
执行顺序: 多次
inertia_share
调用会自上而下合并。如果子控制器与父控制器共享同一个键,子控制器的值会覆盖父控制器的。块和lambda形式的共享数据会按请求延迟执行——非Inertia请求不会触发执行。

Flash Messages

Flash 消息

Flash is automatic. Configure exposed keys if needed:
ruby
undefined
Flash消息会自动处理。如需暴露自定义键,请进行配置:
ruby
undefined

config/initializers/inertia_rails.rb

config/initializers/inertia_rails.rb

InertiaRails.configure do |config| config.flash_keys = %i[notice alert toast] # default: %i[notice alert] end

Use standard Rails flash in controllers:
```ruby
redirect_to users_path, notice: "User created!"
InertiaRails.configure do |config| config.flash_keys = %i[notice alert toast] # 默认值:%i[notice alert] end

在控制器中使用标准Rails flash:
```ruby
redirect_to users_path, notice: "用户创建成功!"

or

或者

flash.alert = "Something went wrong" redirect_to users_path
undefined
flash.alert = "出现错误" redirect_to users_path
undefined

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:
ruby
def create
  @user = User.new(user_params)
  if @user.save
    redirect_to users_path, notice: "Created!"
  else
    redirect_back_or_to new_user_path, inertia: { errors: @user.errors.to_hash(true) }
  end
end
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.
创建/更新/删除操作完成后,始终使用重定向(Post-Redirect-Get模式)。标准Rails的
redirect_to
可以正常工作。Inertia特有的部分是验证错误处理:
ruby
def create
  @user = User.new(user_params)
  if @user.save
    redirect_to users_path, notice: "创建成功!"
  else
    redirect_back_or_to new_user_path, inertia: { errors: @user.errors.to_hash(true) }
  end
end
to_hash
to_hash(true)
的区别:
to_hash
返回
{ name: ["can't be blank"] }
to_hash(true)
返回
{ name: ["Name can't be blank"] }
。键必须与输入框的
name
属性匹配——键不匹配会导致错误无法显示在对应字段旁。
绝对不要使用
errors.full_messages
——它会生成无字段键的扁平字符串,导致前端无法将错误映射到对应的输入字段。

Authorization as Props

作为Prop的授权信息

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
(~40 lines) — full-stack
can
pattern with Action Policy/Pundit/CanCanCan examples.
Do NOT load if not passing permission data to the frontend.
按资源传递
can
哈希形式的权限——前端控制可见性,服务端强制执行访问控制。请查看
inertia-rails-controllers
+
inertia-rails-pages
技能文档。
强制要求——实现授权prop时请通读整个文件:
references/authorization.md
(约40行)——包含Action Policy/Pundit/CanCanCan示例的全栈
can
模式。
如果不需要向前端传递权限数据,请不要加载此内容。

External Redirects (
inertia_location
)

外部重定向(
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
.
ruby
undefined
重点注意: 对外部URL使用
redirect_to
会破坏Inertia——客户端收到302状态码后会尝试将其作为Inertia响应(JSON)处理,而非整页重定向。
inertia_location
返回409状态码和
X-Inertia-Location
请求头,告知客户端执行
window.location = url
ruby
undefined

Stripe checkout — MUST use inertia_location, not redirect_to

Stripe结账——必须使用inertia_location,不能使用redirect_to

def create checkout_session = Current.user.payment_processor.checkout( mode: "payment", line_items: "price_xxx", success_url: enrollments_url, cancel_url: course_url(@course), ) inertia_location checkout_session.url end

Use `inertia_location` for any URL outside the Inertia app: payment
providers, OAuth, external services.
def create checkout_session = Current.user.payment_processor.checkout( mode: "payment", line_items: "price_xxx", success_url: enrollments_url, cancel_url: course_url(@course), ) inertia_location checkout_session.url end

对所有Inertia应用外的URL使用`inertia_location`:支付服务商、OAuth、外部服务等。

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
.
对浏览器历史记录中的页面数据进行加密——
config.encrypt_history = Rails.env.production?
。 在登出/角色变更时使用
redirect_to path, inertia: { clear_history: true }
。 包含服务端和客户端示例的完整设置请查看
references/configuration.md

Configuration

配置

See
references/configuration.md
for all
InertiaRails.configure
options (version, encrypt_history, flash_keys, etc.).
所有
InertiaRails.configure
选项(版本、encrypt_history、flash_keys等)请查看
references/configuration.md

Troubleshooting

故障排查

SymptomCauseFix
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 fieldsError 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 routesRun
rails js_routes:generate
after changing
config/routes.rb
症状原因修复方案
Stripe/OAuth重定向时出现302循环对外部URL使用了
redirect_to
使用
inertia_location
——它返回409状态码 +
X-Inertia-Location
请求头
错误无法显示在对应字段旁错误键与输入框
name
不匹配
to_hash
的键必须与输入框
name
属性完全匹配
TS2305:
postsPath
@/routes
中找不到
添加路由后未重新生成js-routes修改
config/routes.rb
后执行
rails js_routes:generate

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
  • 表单错误展示
    inertia-rails-forms
  • Flash消息提示UI
    inertia-rails-pages
    (权限) +
    shadcn-inertia
    (Sonner组件)
  • 客户端延迟处理
    inertia-rails-pages
    <Deferred>
    组件)
  • 类型安全Prop
    inertia-rails-typescript
    alba-inertia
    (序列化器)
  • 测试
    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
(~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
(~180 lines) only when setting up
InertiaRails.configure
for the first time or debugging configuration issues. Do NOT load for routine controller work.
强制要求——使用高级prop类型(
merge
scroll
deep_merge
)或组合多个prop选项时请通读整个文件:
references/prop-types.md
(约180行)——包含所有prop类型的详细行为、边界情况和组合规则。
如果仅使用基础的
defer
optional
once
always
,无需加载
prop-types.md
——上述表格内容已足够。
仅在首次设置
InertiaRails.configure
或排查配置问题时加载
references/configuration.md
(约180行)。日常控制器开发无需加载此文件。