inertia-rails-controllers by inertia-rails/skills
npx skills add https://github.com/inertia-rails/skills --skill inertia-rails-controllers用于服务 Inertia 响应的 Rails 控制器服务器端模式。
在添加属性前,请先思考:
InertiaController)中使用 inertia_share,而不是作为每个动作的属性InertiaRails.defer — 页面快速加载,数据随后流式加载InertiaRails.optional — 在初始加载时跳过InertiaRails.once — 在导航间缓存绝对不要:
redirect_to — 它会返回 302 状态码,但 Inertia 客户端会尝试将响应解析为 JSON,导致重定向失败。请使用 (返回 409 状态码和 响应头)。广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
inertia_locationX-Inertia-Locationerrors.full_messages — 它会生成没有字段键的扁平字符串,导致前端无法将错误映射到对应的输入字段。请使用 errors.to_hash(true)。inertia.defer、Inertia.defer 或 inertia_rails.defer — 正确的语法是 InertiaRails.defer { ... }。所有属性辅助方法都是 InertiaRails 常量上的模块方法。alba-inertia gem)。每个需要向前端传递属性的动作必须调用 render inertia: { key: data }。config.flash_keys 的情况下使用 success/error 作为闪存键 — Rails 默认使用 notice/alert。自定义键必须同时添加到初始化器配置和 FlashData TypeScript 类型中。
default_render: true陷阱: 此设置仅根据控制器/动作自动推断组件名称 — 它不会自动将实例变量作为属性传递。在启用了default_render: true的动作中编写@posts = Post.all会渲染正确的组件,但向前端发送零数据。只有在配置了alba-inertiagem 时,实例变量才会自动序列化为属性 — 在依赖此功能前,请检查Gemfile。如果没有配置,你必须使用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)。
# 正确 — 数据作为属性传递
def index
render inertia: { users: users_data, stats: InertiaRails.defer { ExpensiveQuery.run } }
end
# 正确 — 静态页面,不需要数据
def index; end
# 错误 — @posts 永远不会发送到前端(没有 alba-inertia 的情况下)
def index
@posts = Post.all
end
注意: 如果项目使用了
alba-inertiagem(检查Gemfile),实例变量会自动序列化为属性,不需要显式的render inertia:。关于该约定,请参阅alba-inertia技能。
InertiaRails.defer — 不是 inertia.defer,也不是 Inertia.defer。所有属性辅助方法都是 InertiaRails 上的模块方法。
| 类型 | 语法 | 行为 |
|---|---|---|
| 常规 | { key: value } | 总是求值,总是包含 |
| 惰性 | -> { expensive_value } | 包含在初始页面渲染中,在部分重载时惰性求值 |
| 可选 | InertiaRails.optional { ... } | 仅在请求它的部分重载时求值 |
| 延迟 | InertiaRails.defer { ... } | 在初始页面渲染后加载 |
| 延迟(分组) | InertiaRails.defer(group: 'name') { ... } | 分组延迟 — 并行获取 |
| 一次性 | InertiaRails.once { ... } | 解析一次,在导航间记住 |
| 合并 | InertiaRails.merge { ... } | 追加到现有数组(无限滚动) |
| 深度合并 | InertiaRails.deep_merge { ... } | 深度合并到现有对象中 |
| 总是 | InertiaRails.always { ... } | 即使在部分重载中也包含 |
| 滚动 | InertiaRails.scroll { ... } | 用于无限滚动的滚动感知属性 |
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
服务器延迟加载慢速数据,客户端显示回退内容然后替换为实际内容:
# 控制器
def show
render inertia: {
basic_stats: Stats.quick_summary,
analytics: InertiaRails.defer { Analytics.compute_slow },
}
end
// 页面组件 — 子组件从页面属性中读取延迟属性
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>
}
在控制器中使用 inertia_share — 它需要控制器上下文(current_user、请求)。初始化器只处理 config.* 设置(版本、flash_keys)。
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 请求运行。
闪存是自动的。如果需要,可以配置暴露的键:
# config/initializers/inertia_rails.rb
InertiaRails.configure do |config|
config.flash_keys = %i[notice alert toast] # 默认: %i[notice alert]
end
在控制器中使用标准的 Rails 闪存:
redirect_to users_path, notice: "用户已创建!"
# 或
flash.alert = "出错了"
redirect_to users_path
在创建/更新/删除操作后,始终进行重定向(Post-Redirect-Get)。标准的 Rails redirect_to 有效。Inertia 特定的部分是验证错误处理:
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: ["不能为空"] },to_hash(true) 给出 { name: ["姓名不能为空"] }。键必须与输入字段的 name 属性匹配 — 键不匹配意味着错误不会显示在正确的字段旁边。
绝对不要使用 errors.full_messages — 它会生成没有字段键的扁平字符串,导致前端无法将错误映射到对应的输入字段。
将权限作为每个资源的 can 哈希传递 — 前端控制可见性,服务器强制执行访问。请参阅 inertia-rails-controllers + inertia-rails-pages 技能。
强制要求 — 实现授权属性时请完整阅读文件: references/authorization.md(约 40 行)— 包含 Action Policy/Pundit/CanCanCan 示例的全栈 can 模式。
如果不需要向前端传递权限数据,请不要加载。
inertia_location)关键: 对外部 URL 使用 redirect_to 会破坏 Inertia — 客户端收到 302 状态码,但会尝试将其作为 Inertia 响应(JSON)处理,而不是完整的页面重定向。inertia_location 返回 409 状态码和 X-Inertia-Location 响应头,这告诉客户端执行 window.location = url。
# 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
对 Inertia 应用之外的任何 URL 使用 inertia_location:支付提供商、OAuth、外部服务。
在浏览器历史状态中加密页面数据 — config.encrypt_history = Rails.env.production?。在登出/角色变更时使用 redirect_to path, inertia: { clear_history: true }。包含服务器端和客户端示例的完整设置在 references/configuration.md 中。
所有 InertiaRails.configure 选项(版本、encrypt_history、flash_keys 等)请参阅 references/configuration.md。
| 症状 | 原因 | 修复方法 |
|---|---|---|
| Stripe/OAuth 重定向时出现 302 循环 | 对外部 URL 使用了 redirect_to | 使用 inertia_location — 它返回 409 状态码和 X-Inertia-Location 响应头 |
| 错误不显示在字段旁边 | 错误键与输入字段的 name 不匹配 | to_hash 的键必须与输入字段的 name 属性完全匹配 |
TS2305: @/routes 中找不到 postsPath | 添加路由后 js-routes 未重新生成 | 更改 config/routes.rb 后运行 rails js_routes:generate |
inertia-rails-formsinertia-rails-pages(访问) + shadcn-inertia(Sonner)inertia-rails-pages(<Deferred> 组件)inertia-rails-typescript 或 alba-inertia(序列化器)inertia-rails-testing强制要求 — 使用高级属性类型(merge、scroll、deep_merge)或组合多个属性选项时请完整阅读文件: references/prop-types.md(约 180 行)— 所有属性类型的详细行为、边界情况和组合规则。
对于基本的 defer、optional、once 或 always 用法,请不要加载 prop-types.md — 上表已足够。
仅在首次设置 InertiaRails.configure 或调试配置问题时加载 references/configuration.md(约 180 行)。对于常规的控制器工作,请不要加载。
每周安装次数
76
代码仓库
GitHub 星标数
35
首次出现
2026年2月13日
安全审计
安装于
codex74
gemini-cli74
opencode74
github-copilot73
amp72
kimi-cli71
Server-side patterns for Rails controllers serving Inertia responses.
Before adding a prop, ask:
inertia_share in a base controller (InertiaController), not a per-action propInertiaRails.defer — page loads fast, data streams in afterInertiaRails.optional — skipped on initial loadInertiaRails.once — cached across navigationsNEVER:
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).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).inertia.defer, Inertia.defer, or inertia_rails.defer — the correct syntax is InertiaRails.defer { ... }. All prop helpers are module methods on the InertiaRails constant.alba-inertia gem is configured). Every action that passes props to the frontend MUST call render inertia: { key: data }.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.
default_render: trueTRAP: This setting only auto-infers the component name from controller/action — it does NOT auto-pass instance variables as props. Writing@posts = Post.allin an action withdefault_render: truerenders the correct component but sends zero data to the frontend. Instance variables are only auto-serialized as props whenalba-inertiagem is configured — checkGemfilebefore relying on this. Without it, you MUST userender 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 callrender 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).
# CORRECT — data passed as props
def index
render inertia: { users: users_data, stats: InertiaRails.defer { ExpensiveQuery.run } }
end
# CORRECT — static page, no data needed
def index; end
# WRONG — @posts is NEVER sent to the frontend (without alba-inertia)
def index
@posts = Post.all
end
Note: If the project uses the
alba-inertiagem (checkGemfile), instance variables are auto-serialized as props and explicitrender inertia:is not needed. See thealba-inertiaskill for that convention.
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') { ... } |
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
Server defers slow data, client shows fallback then swaps in content:
# Controller
def show
render inertia: {
basic_stats: Stats.quick_summary,
analytics: InertiaRails.defer { Analytics.compute_slow },
}
end
// 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>
}
Use inertia_share in controllers — it needs controller context (current_user, request). The initializer only handles config.* settings (version, flash_keys).
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.
Flash is automatic. Configure exposed keys if needed:
# 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:
redirect_to users_path, notice: "User created!"
# or
flash.alert = "Something went wrong"
redirect_to users_path
After create/update/delete, always redirect (Post-Redirect-Get). Standard Rails redirect_to works. The Inertia-specific part is validation error handling:
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 useerrors.full_messages — it produces flat strings without field keys, so errors can't be mapped to the corresponding input fields on the frontend.
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.
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.
# Stripe checkout — MUST use inertia_location, not 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.
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.
See references/configuration.md for all InertiaRails.configure options (version, encrypt_history, flash_keys, etc.).
| 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 |
inertia-rails-formsinertia-rails-pages (access) + shadcn-inertia (Sonner)inertia-rails-pages (<Deferred> component)inertia-rails-typescript or alba-inertia (serializers)inertia-rails-testingMANDATORY — 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.
Weekly Installs
76
Repository
GitHub Stars
35
First Seen
Feb 13, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex74
gemini-cli74
opencode74
github-copilot73
amp72
kimi-cli71
TanStack Query v5 完全指南:React 数据管理、乐观更新、离线支持
2,500 周安装
Microsoft 代码参考工具 - 查找API、代码示例与错误排查,提升Azure开发效率
8,800 周安装
如何创建AGENTS.md文件 - AI编码助手项目文档指南与模板
8,800 周安装
AI项目架构蓝图生成器:自动分析代码生成架构文档,支持.NET/Java/React等
8,700 周安装
Better Auth 电子邮件与密码最佳实践:安全登录、验证与密码重置配置指南
8,900 周安装
Defuddle CLI:网页内容提取工具,一键移除广告导航,节省AI令牌使用量
9,200 周安装
Penpot UI/UX 设计指南:使用 MCP 服务器和 AI 辅助工具进行专业设计
8,900 周安装
| 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 |
@/routes| js-routes not regenerated after adding routes |
Run rails js_routes:generate after changing config/routes.rb |