重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
inertia-rails-forms by inertia-rails/skills
npx skills add https://github.com/inertia-rails/skills --skill inertia-rails-forms适用于 Inertia.js + Rails 的全栈表单处理。
构建表单前,请思考:
<Form> 组件(无需状态管理)。<Form>。用于 UI 状态(预览 URL、文件大小显示)的 React useState 独立于表单数据——<Form> 处理提交;useState 处理 UI。useForm 钩子。<Form> 已经处理了 CSRF 令牌、重定向跟随、来自 Rails 的错误映射、处理状态、文件上传检测和历史状态。react-hook-form 会重复或干扰所有这些功能。广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
<Form> 或 useForm:router.get 配合防抖 + preserveState,或直接使用 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 —— 它是一个钩子(适用 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 ? '创建中...' : '创建用户'}
</button>
</>
)}
</Form>
)
}
// 普通子元素 —— 有效但无法访问 errors/processing/progress:
// <Form method="post" action="/users">
// <input name="name" />
// <button type="submit">创建</button>
// </Form>
<Form method="delete" action={`/posts/${post.id}`}>
{({ processing }) => (
<button type="submit" disabled={processing}>
{processing ? '删除中...' : '删除文章'}
</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" 和非受控默认值:
defaultValuedefaultChecked<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} />
已发布
</label>
<button type="submit" disabled={processing}>
{processing ? '保存中...' : '更新文章'}
</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()}>提交</button>
<button onClick={() => formRef.current?.reset()}>重置</button>
</>
)
}
useForm 钩子仅将 useForm 用于多步骤向导、动态添加/删除字段或与兄弟组件共享表单数据。
强制要求 —— 使用 useForm 钩子、transform、errorBag、resetOnSuccess、多步骤表单或使用 setError 进行客户端验证时,请完整阅读此文件: references/advanced-forms.md (约 330 行)—— 完整的 useForm API、转换示例、错误包作用域、多步骤向导模式和客户端验证。
不要加载 advanced-forms.md 当使用 <Form> 组件处理简单的创建/编辑表单时——上面的示例已经足够。
<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 ? '上传中...' : '保存'}
</button>
</>
)}
</Form>
)
}
为上传选择 <Form> 还是 useForm:
<Form> 内<Form> + 在 change 事件上调用 formRef.submit()useForm(拖放的文件不在 DOM 输入框中,setData 更清晰)预览/验证 → 在任何一种方法旁边使用 useState,请参阅 references/file-uploads.md。
以上所有示例均使用 React 语法。Vue 3 或 Svelte 的等效用法:
references/vue.md —— 带有 #default 作用域插槽的 <Form>,useForm 返回响应式代理(form.email 而非 setData),v-model 绑定references/svelte.md —— 带有 {#snippet} 语法的 <Form>,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 被误认为是"编程式数据操作" | 预览 UI 使用 <Form> + useState。仅当表单提交数据必须存在于 React 状态中(多步骤、动态添加/删除字段)时才需要 useForm。文件预览是本地 UI 状态,不是表单数据 |
inertia-rails-controllers(redirect_back、to_hash、flash)shadcn-inertia(Input/Select 适配、toast UI)inertia-rails-typescript(使用 type Props 而非 interface,TS2344)强制要求 —— 处理带有图片预览、Active Storage 或直接上传的文件上传时,请完整阅读此文件: references/file-uploads.md (约 200 行)—— 使用 <Form> 的图片预览、Active Storage 集成、直接上传设置、多文件处理和进度跟踪。
每周安装量
70
代码仓库
GitHub 星标数
35
首次出现
2026年2月13日
安全审计
已安装于
gemini-cli69
opencode69
codex69
github-copilot68
amp67
kimi-cli66
Full-stack form handling for Inertia.js + Rails.
Before building a form, ask:
<Form> component (no state management needed)<Form>. React useState for UI state (preview URL, file size display) is independent of form data — <Form> handles the submission; useState handles the UI.useForm hook<Form> already handles CSRF tokens, redirect following, error mapping from Rails, processing state, file upload detection, and history state. react-hook-form would duplicate or fight all of this.When NOT to use<Form> or useForm:
router.get with debounce + preserveState, or raw fetch for large datasetsrouter.patch directly, or useForm if you need error display on the fieldNEVER:
react-hook-form, vee-validate, or sveltekit-superforms — Inertia <Form> already handles CSRF, redirect following, error mapping, processing state, and file detection. These libraries fight Inertia's request lifecycle.data={...} to <Form> — it has no data prop. Data comes from input name attributes automatically. data is a useForm concept.useForm for simple create/edit — handles these without state management. Reserve for multi-step wizards, dynamic add/remove fields, or form data shared with sibling components.<Form> Component (Preferred)The simplest way to handle forms. Collects data from input name attributes automatically — no manual state management needed. <Form> has NO data prop — do NOT pass data={...} (that's a useForm concept). For edit forms, use defaultValue on inputs.
Use render function children {({ errors, processing }) => (...)} to access form state. Plain children work but give no access to errors, processing, or progress.
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>
| Property | Type | Purpose |
|---|---|---|
errors | Record<string, string> | Validation errors keyed by field name |
processing | boolean | True while request is in flight |
progress | `{ percentage: number } | null` |
hasErrors | boolean |
Additional <Form> props (errorBag, only, resetOnSuccess, event callbacks like onBefore, onSuccess, onError, onProgress) are documented in references/advanced-forms.md — see loading trigger below.
Use method="patch" and uncontrolled defaults:
defaultValuedefaultCheckeddefaultValue on <select>Checkbox without explicit
valuesubmits"on"— setvalue="1"so Rails casts to boolean correctly.
<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>
Use the transform prop to reshape data before submission without useForm.
For advanced transform with useForm, see references/advanced-forms.md.
formRefThe ref exposes the same methods and state as render function props (FormComponentSlotProps). Use when you need to interact with the form from outside <Form>. Key ref methods: submit(), reset(), clearErrors(), setError(), getData(), getFormData(), validate(), touch(), defaults(). State: , , , , , , .
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 HookUse useForm only for multi-step wizards, dynamic add/remove fields, or form data shared with sibling components.
MANDATORY — READ ENTIRE FILE when using useForm hook, transform, errorBag, resetOnSuccess, multi-step forms, or client-side validation with setError: references/advanced-forms.md (~330 lines) — full useForm API, transform examples, error bag scoping, multi-step wizard patterns, and client-side validation.
Do NOT load advanced-forms.md when using <Form> component for simple create/edit forms — the examples above are sufficient.
Both <Form> and useForm auto-detect files and switch to FormData. Upload progress is built into the render function — destructure progress alongside errors and processing:
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>
)
}
Choosing<Form> vs useForm for uploads:
<Form><Form> + formRef.submit() on changeuseForm (dropped files aren't in DOM inputs, setData is cleaner)Preview / validation → useState alongside either approach, see references/file-uploads.md.
All examples above use React syntax. For Vue 3 or Svelte equivalents:
references/vue.md — <Form> with #default scoped slot, useForm returns reactive proxy (form.email not setData), v-model bindingreferences/svelte.md — <Form> with {#snippet} syntax, returns Writable store (), , ref exposes methods only (not reactive state)MANDATORY — READ THE MATCHING FILE when the project uses Vue or Svelte.
| Symptom | Cause | Fix |
|---|---|---|
No access to errors/processing | Plain children instead of render function | <Form> children should be {({ errors, processing }) => (...)} |
| Form sends GET instead of POST | Missing method prop | Add method="post" (or "patch", "delete") |
inertia-rails-controllers (redirect_back, to_hash, flash)shadcn-inertia (Input/Select adaptation, toast UI)inertia-rails-typescript (type Props not interface, TS2344)MANDATORY — READ ENTIRE FILE when handling file uploads with image preview, Active Storage, or direct uploads: references/file-uploads.md (~200 lines) — image preview with <Form>, Active Storage integration, direct upload setup, multiple files, and progress tracking.
Weekly Installs
70
Repository
GitHub Stars
35
First Seen
Feb 13, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
gemini-cli69
opencode69
codex69
github-copilot68
amp67
kimi-cli66
React视图过渡API使用指南:实现原生浏览器动画与状态管理
8,400 周安装
<Form>useFormvalue= instead of defaultValue on inputs — controlled inputs bypass <Form>'s dirty tracking, making isDirty always false.value="1" on checkboxes — without it, the browser submits "on" and Rails won't cast to boolean correctly.useForm inside a loop or conditional — it's a hook (React rules apply). Create one form instance per logical form.| True if any errors exist |
wasSuccessful | boolean | True after last submit succeeded |
recentlySuccessful | boolean | True for 2s after success — ideal for "Saved!" feedback |
isDirty | boolean | True if any input changed from initial value |
reset | (...fields) => void | Reset specific fields or all fields |
clearErrors | (...fields) => void | Clear specific errors or all errors |
errorsprocessingprogressisDirtyhasErrorswasSuccessfulrecentlySuccessfuluseForm$form.emailbind:value| File upload sends empty body | PUT/PATCH with file | Multipart limitation — Inertia auto-adds _method field to convert to POST |
| Errors don't clear after fixing field | Stale error state | Errors auto-clear on next submit; use clearErrors('field') for immediate clearing |
isDirty always false | Using value instead of defaultValue | Controlled inputs (value=) bypass dirty tracking — use defaultValue |
progress is always null | No file input in form | Progress tracking only activates when <Form> detects a file input |
Checkbox sends "on" | No explicit value | Add value="1" to checkbox inputs |
| Form submits twice in dev | React StrictMode double-invocation | Normal in development — StrictMode remounts components. Only fires once in production |
Used useForm for file upload with preview | onChange + useState mistaken for "programmatic data manipulation" | <Form> + useState for preview UI. useForm is only needed when form submission data must live in React state (multi-step, dynamic add/remove fields). File preview is local UI state, not form data |