关于 inertia-rails-forms
面向 Inertia.js 与 Rails 表单开发的技能指南,覆盖 React、Vue 和 Svelte。说明何时使用 Form 组件或 useForm,并介绍验证、文件上传及常见问题。
主要功能
- 01选择 Form 或 useForm
- 02处理 Rails 验证错误
- 03文件上传与进度显示
- 04框架专属指导
适用场景
- 01构建简单的创建和编辑表单
- 02实现多步骤或动态表单
- 03添加带进度的文件上传
- 04排查表单状态与提交问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ inertia-rails/ skills --skill inertia-rails-forms安装后在 Claude Code 中输入/inertia-rails-forms确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/advanced-forms.mdfile-uploads.mdsvelte.mdvue.md详细介绍
Inertia Rails 表单
适用于 Inertia.js + Rails 的全栈表单处理。
创建表单前,先确认:
- 简单的创建/编辑? → 使用
<Form>组件(无需状态管理) - 需要为每个字段设置 UI 元素? → 仍使用
<Form>。 ReactuseState用于 UI 状态(预览 URL、文件大小显示),与表单数据相互独立——<Form>处理提交;useState处理 UI。 - 多步向导、动态字段(添加/移除输入项),或需要与同级组件共享表单数据(例如实时预览面板)? → 使用
useFormhook - 考虑使用 react-hook-form? → 不要这样做。Inertia 的
<Form>已处理 CSRF 令牌、重定向跟进、Rails 错误映射、处理中状态、文件上传检测和历史状态。react-hook-form 会重复或干扰这些功能。
以下情况不要使用 <Form> 或 useForm:
- 数据查询——这不是表单提交。使用带防抖和
preserveState的router.get,或对大型数据集使用原始fetch - 无需导航的单字段内联编辑——直接使用
router.patch;如果需要显示字段错误,则使用useForm
绝对不要:
- 使用
react-hook-form、vee-validate或sveltekit-superforms——Inertia<Form>已处理 CSRF、重定向跟进、错误映射、处理中状态和文件检测。这些库会干扰 Inertia 的请求生命周期。 - 向
<Form>传递data={...}——它没有data属性。数据会自动从输入项的name属性获取。data是useForm的概念。 - 对简单的创建/编辑使用
useForm——<Form>无需状态管理即可处理这些场景。仅在多步向导、动态添加/移除字段,或需要与同级组件共享表单数据时使用useForm。 - 在输入项上使用受控的
value=,而不是defaultValue——受控输入会绕过<Form>的脏状态跟踪,导致isDirty始终为false。 - 在复选框上省略
value="1"——否则浏览器会提交"on",Rails 无法正确转换为布尔值。 - 在循环或条件语句中调用
useForm——它是一个 hook(需遵循 React 规则)。每个逻辑表单创建一个表单实例。
<Form> 组件(首选)
处理表单最简单的方式。会自动从输入项的 name 属性收集数据——无需手动管理状态。<Form> 没有 data 属性——不要传递 data={...}(这是 useForm 的概念)。对于编辑表单,在输入项上使用 defaultValue。
使用渲染函数子项 {({ errors, processing }) => (...)} 访问表单状态。普通子项也能使用,但无法访问错误、处理中状态或进度。
import { Form } from '@inertiajs/react'
export default function CreateUser() {
return (
<Form method="post" action="/users">
{({ errors, processing }) => (
<>
<input type="text" name="name" />
{errors.name && <span className="error">{errors.name}</span>}
<input type="email" name="email" />
{errors.email && <span className="error">{errors.email}</span>}
<button type="submit" disabled={processing}>
{processing ? 'Creating...' : 'Create User'}
</button>
</>
)}
</Form>
)
}
// Plain children — valid but no access to errors/processing/progress:
// <Form method="post" action="/users">
// <input name="name" />
// <button type="submit">Create</button>
// </Form>
删除表单
<Form method="delete" action={`/posts/${post.id}`}>
{({ processing }) => (
<button type="submit" disabled={processing}>
{processing ? 'Deleting...' : 'Delete Post'}
</button>
)}
</Form>
渲染函数的关键属性
| 属性 | 类型 | 用途 |
|---|---|---|
errors | Record<string, string> | 按字段名称索引的验证错误 |
processing | boolean | 请求进行中时为 true |
progress | { percentage: number } | null | 上传进度(仅限文件上传) |
hasErrors | boolean | 存在任何错误时为 true |
wasSuccessful | boolean | 上次提交成功后为 true |
recentlySuccessful | boolean | 成功后 2 秒内为 true,适合显示“已保存!”提示 |
isDirty | boolean | 任一输入项的值与初始值不同时为 true |
reset | (...fields) => void | 重置指定字段或所有字段 |
clearErrors | (...fields) => void | 清除指定错误或所有错误 |
其他 <Form> 属性(errorBag、only、resetOnSuccess,以及 onBefore、onSuccess、onError、onProgress 等事件回调)见 references/advanced-forms.md——请参阅下方的加载触发器。
编辑表单(预填充)
使用 method="patch" 和非受控默认值:
- 文本框/文本区域 →
defaultValue - 复选框/单选按钮 →
defaultChecked - 选择框 → 在
<select>上设置defaultValue
未显式设置
value的复选框会提交"on"——设置value="1",以便 Rails 正确转换为布尔值。
<Form method="patch" action={`/posts/${post.id}`}>
{({ errors, processing }) => (
<>
<input type="text" name="title" defaultValue={post.title} />
{errors.title && <span className="error">{errors.title}</span>}
<label>
<input type="checkbox" name="published" value="1"
defaultChecked={post.published} />
Published
</label>
<button type="submit" disabled={processing}>
{processing ? 'Saving...' : 'Update Post'}
</button>
</>
)}
</Form>
转换数据
使用 transform 属性可在提交前重塑数据,无需使用 useForm。
有关 useForm 中高级 transform 用法,请参阅 references/advanced-forms.md。
使用 formRef 进行外部访问
该 ref 会公开与渲染函数属性(FormComponentSlotProps)相同的方法和状态。
当你需要在 <Form> 外部与表单交互时使用。
主要 ref 方法:submit()、reset()、clearErrors()、setError()、
getData()、getFormData()、validate()、touch()、defaults()。
状态:errors、processing、progress、isDirty、hasErrors、
wasSuccessful、recentlySuccessful。
import {useRef} from 'react'
import {Form} from '@inertiajs/react'
import type {FormComponentRef} from '@inertiajs/core'
export default function CreateUser() {
const formRef = useRef<FormComponentRef>(null)
return (
<>
<Form ref={formRef} method='post' action='/users'>
{({errors}) => (
<>
<input type='text' name='name'/>
{errors.name && <span className='error'>{errors.name}</span>}
</>
)}
</Form>
<button onClick={() => formRef.current?.submit()}>Submit</button>
<button onClick={() => formRef.current?.reset()}>Reset</button>
</>
)
}
useForm Hook
仅在多步骤向导、动态添加/移除字段,或需要与同级组件共享表单数据时使用 useForm。
必须 — 使用 useForm Hook、transform、errorBag、resetOnSuccess、多步骤表单,或使用 setError 进行客户端验证时,请阅读整个文件:
references/advanced-forms.md(约 330 行)——完整的
useForm API、transform 示例、错误包作用域、多步骤向导
模式,以及客户端验证。
使用 <Form> 组件创建简单的新增/编辑表单时,请勿加载 advanced-forms.md——上面的示例已足够。
文件上传
<Form> 和 useForm 都会自动检测文件并切换到 FormData。
上传进度已内置于渲染函数中——与 errors 和 processing 一起解构 progress:
type Props = { user: User }
export default function EditProfile({ user }: Props) {
return (
<Form method="patch" action="/profile">
{({ errors, processing, progress }) => (
<>
<input type="text" name="name" defaultValue={user.name} />
{errors.name && <span className="error">{errors.name}</span>}
<input type="file" name="avatar" />
{errors.avatar && <span className="error">{errors.avatar}</span>}
{progress && (
<progress value={progress.percentage ?? 0} max="100" />
)}
<button type="submit" disabled={processing}>
{processing ? 'Uploading...' : 'Save'}
</button>
</>
)}
</Form>
)
}
选择用于上传的 <Form> 或 useForm:
- 文件与其他字段一起提交(头像 + 姓名,一个“保存”按钮)→ 在
<Form>内放置文件输入框 - 独立的即时上传(选择后立即上传,无“保存”按钮)→ 使用
<Form>+ 在 change 时调用formRef.submit() - 拖放上传 → 使用
useForm(拖放的文件不在 DOM 输入框中,使用setData更简洁)
预览/验证 → 在任一方案中配合使用 useState,请参阅
references/file-uploads.md。
Vue / Svelte
以上示例均使用 React 语法。Vue 3 或 Svelte 的等效用法:
- Vue 3:
references/vue.md—<Form>使用#default作用域插槽,useForm返回响应式代理(使用form.email而非setData),v-model绑定 - Svelte:
references/svelte.md—<Form>使用{#snippet}语法,useForm返回 Writable store($form.email),bind:value,ref 仅公开方法(不包括响应式状态)
必须 — 项目使用 Vue 或 Svelte 时,请阅读对应文件。
故障排除
| 症状 | 原因 | 解决方法 |
|---|---|---|
无法访问 errors/processing | 使用了普通子元素,而非渲染函数 | <Form> 子元素应为 {({ errors, processing }) => (...)} |
| 表单发送 GET 而非 POST | 缺少 method 属性 | 添加 method="post"(或 "patch"、"delete") |
| 文件上传发送空请求体 | 使用 PUT/PATCH 上传文件 | 多部分表单限制——Inertia 会自动添加 _method 字段,将请求转换为 POST |
| 修正字段后错误未清除 | 错误状态过期 | 错误会在下次提交时自动清除;如需立即清除,请使用 clearErrors('field') |
isDirty 始终为 false | 使用了 value 而非 defaultValue | 受控输入框(value=)会绕过脏状态跟踪——请使用 defaultValue |
progress 始终为 null | 表单中没有文件输入框 | 只有当 <Form> 检测到文件输入框时,才会启用进度跟踪 |
复选框发送 "on" | 未显式设置 value | 为复选框输入框添加 value="1" |
| 开发环境中表单提交两次 | React StrictMode 双重调用 | 开发环境中的正常现象——StrictMode 会重新挂载组件。生产环境中只会触发一次 |
使用 useForm 上传文件并预览 | 将 onChange + useState 误认为“程序化数据操作” | 使用 <Form> + useState 实现预览 UI。只有当表单提交数据必须存储在 React 状态中(多步骤、动态添加/移除字段)时,才需要 useForm。文件预览属于本地 UI 状态,而非表单数据 |
相关技能
- 服务端 PRG 与错误处理 →
inertia-rails-controllers(redirect_back、to_hash、flash) - shadcn 输入控件 →
shadcn-inertia(Input/Select 适配、toast UI) - 页面 props 类型 →
inertia-rails-typescript(使用type Props而非interface,TS2344)
必须 — 处理带图片预览的文件上传、Active Storage 或直接上传时,请阅读整个文件:
references/file-uploads.md(约 200 行)——使用 <Form> 进行图片预览、Active Storage 集成、直接上传设置、多文件和进度跟踪。