inertia-rails-forms

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Inertia Rails Forms

Inertia Rails 表单处理

Full-stack form handling for Inertia.js + Rails.
Before building a form, ask:
  • Simple create/edit?
    <Form>
    component (no state management needed)
  • Requires per-field UI elements? → Still
    <Form>
    . React
    useState
    for UI state (preview URL, file size display) is independent of form data —
    <Form>
    handles the submission;
    useState
    handles the UI.
  • Multi-step wizard, dynamic fields (add/remove inputs), or form data shared with sibling components (e.g., live preview panel)?
    useForm
    hook
  • Tempted by react-hook-form? → Don't. Inertia's
    <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
:
  • Data lookups — not a form submission. Use
    router.get
    with debounce +
    preserveState
    , or raw
    fetch
    for large datasets
  • Inline single-field edits without navigation
    router.patch
    directly, or
    useForm
    if you need error display on the field
NEVER:
  • Use
    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.
  • Pass
    data={...}
    to
    <Form>
    — it has no
    data
    prop. Data comes from input
    name
    attributes automatically.
    data
    is a
    useForm
    concept.
  • Use
    useForm
    for simple create/edit —
    <Form>
    handles these without state management. Reserve
    useForm
    for multi-step wizards, dynamic add/remove fields, or form data shared with sibling components.
  • Use controlled
    value=
    instead of
    defaultValue
    on inputs — controlled inputs bypass
    <Form>
    's dirty tracking, making
    isDirty
    always
    false
    .
  • Omit
    value="1"
    on checkboxes — without it, the browser submits
    "on"
    and Rails won't cast to boolean correctly.
  • Call
    useForm
    inside a loop or conditional — it's a hook (React rules apply). Create one form instance per logical form.
Inertia.js + Rails 的全栈表单处理方案。
在构建表单前,请先确认:
  • 简单的创建/编辑场景? → 使用
    <Form>
    组件(无需状态管理)
  • 需要按字段展示UI元素? → 仍然使用
    <Form>
    组件。 React
    useState
    用于处理UI状态(如预览URL、文件大小显示),与表单数据相互独立 ——
    <Form>
    负责处理提交逻辑;
    useState
    负责UI状态管理。
  • 多步骤向导、动态字段(添加/移除输入框),或表单数据需要与兄弟组件共享(如实时预览面板)? → 使用
    useForm
    钩子
  • 想要使用react-hook-form? → 请勿使用。Inertia的
    <Form>
    已经内置了CSRF 令牌处理、提交后重定向、Rails错误映射、请求处理状态、文件上传 检测以及历史状态管理。react-hook-form会重复实现这些功能,甚至与Inertia的请求生命周期冲突。
不适合使用
<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钩子(需遵循React钩子规则)。每个逻辑表单对应一个表单实例。

<Form>
Component (Preferred)

<Form>
组件(推荐使用)

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.
tsx
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>
处理表单的最简方式。自动从输入框的
name
属性收集数据 —— 无需手动管理状态。
<Form>
没有
data
属性 —— 请勿传递
data={...}
(这是
useForm
的概念)。对于编辑表单,在输入框上使用
defaultValue
使用渲染函数作为子元素
{({ errors, processing }) => (...)}
来 访问表单状态。普通子元素也可使用,但无法获取错误、 请求处理状态或上传进度。
tsx
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>
  )
}

// 普通子元素 —— 合法但无法访问错误/请求处理状态/上传进度:
// <Form method="post" action="/users">
//   <input name="name" />
//   <button type="submit">Create</button>
// </Form>

Delete Form

删除表单

tsx
<Form method="delete" action={`/posts/${post.id}`}>
  {({ processing }) => (
    <button type="submit" disabled={processing}>
      {processing ? 'Deleting...' : 'Delete Post'}
    </button>
  )}
</Form>
tsx
<Form method="delete" action={`/posts/${post.id}`}>
  {({ processing }) => (
    <button type="submit" disabled={processing}>
      {processing ? 'Deleting...' : 'Delete Post'}
    </button>
  )}
</Form>

Key Render Function Properties

渲染函数核心属性

PropertyTypePurpose
errors
Record<string, string>
Validation errors keyed by field name
processing
boolean
True while request is in flight
progress
{ percentage: number } | null
Upload progress (file uploads only)
hasErrors
boolean
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
Additional
<Form>
props (
errorBag
,
only
,
resetOnSuccess
, event callbacks like
onBefore
,
onSuccess
,
onError
,
onProgress
) are documented in
references/advanced-forms.md
— see loading trigger below.
属性类型用途
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
—— 可查看下方的加载触发说明。

Edit Form (Pre-populated)

编辑表单(预填充数据)

Use
method="patch"
and uncontrolled defaults:
  • Text/textarea →
    defaultValue
  • Checkbox/radio →
    defaultChecked
  • Select →
    defaultValue
    on
    <select>
Checkbox without explicit
value
submits
"on"
— set
value="1"
so Rails casts to boolean correctly.
tsx
<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>
使用
method="patch"
和非受控默认值:
  • 文本框/文本域 →
    defaultValue
  • 复选框/单选框 →
    defaultChecked
  • 下拉选择框 → 在
    <select>
    上设置
    defaultValue
未显式设置
value
的复选框会提交
"on"
—— 设置
value="1"
以便Rails 将其正确转换为布尔值。
tsx
<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>

Transforming Data

数据转换

Use the
transform
prop to reshape data before submission without
useForm
.
For advanced
transform
with
useForm
, see
references/advanced-forms.md
.
无需使用
useForm
,直接通过
transform
属性在提交前重塑数据。
关于结合
useForm
的高级
transform
用法,见
references/advanced-forms.md

External Access with
formRef

通过
formRef
外部访问

The 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:
errors
,
processing
,
progress
,
isDirty
,
hasErrors
,
wasSuccessful
,
recentlySuccessful
.
tsx
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>
    </>
  )
}
该Ref暴露与渲染函数属性相同的方法和状态(
FormComponentSlotProps
)。 当需要从
<Form>
外部与表单交互时使用。核心Ref方法:
submit()
reset()
clearErrors()
setError()
getData()
getFormData()
validate()
touch()
defaults()
。状态:
errors
processing
progress
isDirty
hasErrors
wasSuccessful
recentlySuccessful
tsx
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
钩子

Use
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.
仅在多步骤向导、动态添加/移除字段,或 表单数据需要与兄弟组件共享时使用
useForm
**使用
useForm
钩子、
transform
errorBag
resetOnSuccess
、多步骤表单,或使用
setError
进行客户端验证时,**请务必完整阅读以下文档:
references/advanced-forms.md
(约330行)—— 包含完整的
useForm
API、转换示例、错误作用域、多步骤向导模式,以及客户端验证方案。
如果仅使用
<Form>
组件处理简单的创建/编辑表单,无需查看
advanced-forms.md
—— 上述示例已足够。

File Uploads

文件上传

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
:
tsx
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:
  • File submits with other fields (avatar + name, one Save button) → file input inside
    <Form>
  • Standalone immediate upload (uploads on select, no Save button) →
    <Form>
    +
    formRef.submit()
    on change
  • Drag-and-drop upload
    useForm
    (dropped files aren't in DOM inputs,
    setData
    is cleaner)
Preview / validation →
useState
alongside either approach, see
references/file-uploads.md
.
<Form>
useForm
都会自动检测文件并切换为
FormData
格式提交。 渲染函数内置了上传进度追踪功能 —— 与
errors
processing
一起解构
progress
即可:
tsx
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>
    + 选择事件触发
    formRef.submit()
  • 拖拽上传 → 使用
    useForm
    (拖拽的文件不在DOM输入框中,
    setData
    用法更简洁)
预览/验证 → 可结合两种方案使用
useState
,详见
references/file-uploads.md

Vue / Svelte

Vue / Svelte

All examples above use React syntax. For Vue 3 or Svelte equivalents:
  • Vue 3:
    references/vue.md
    <Form>
    with
    #default
    scoped slot,
    useForm
    returns reactive proxy (
    form.email
    not
    setData
    ),
    v-model
    binding
  • Svelte:
    references/svelte.md
    <Form>
    with
    {#snippet}
    syntax,
    useForm
    returns Writable store (
    $form.email
    ),
    bind:value
    , ref exposes methods only (not reactive state)
MANDATORY — READ THE MATCHING FILE when the project uses Vue or Svelte.
上述所有示例使用React语法。Vue 3或Svelte的等效实现:
  • Vue 3
    references/vue.md
    —— 使用
    #default
    作用域插槽的
    <Form>
    useForm
    返回响应式代理(使用
    form.email
    而非
    setData
    ),
    v-model
    绑定
  • Svelte
    references/svelte.md
    —— 使用
    {#snippet}
    语法的
    <Form>
    useForm
    返回可写存储(
    $form.email
    ),
    bind:value
    ,Ref仅暴露方法(不包含响应式状态)
如果项目使用Vue或Svelte,请务必阅读对应的文档。

Troubleshooting

故障排查

SymptomCauseFix
No access to
errors
/
processing
Plain children instead of render function
<Form>
children should be
{({ errors, processing }) => (...)}
Form sends GET instead of POSTMissing
method
prop
Add
method="post"
(or
"patch"
,
"delete"
)
File upload sends empty bodyPUT/PATCH with fileMultipart limitation — Inertia auto-adds
_method
field to convert to POST
Errors don't clear after fixing fieldStale error stateErrors 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 formProgress 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 devReact StrictMode double-invocationNormal 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
症状原因修复方案
无法访问
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状态,不属于表单数据

Related Skills

相关技能

  • Server-side PRG & errors
    inertia-rails-controllers
    (redirect_back, to_hash, flash)
  • shadcn inputs
    shadcn-inertia
    (Input/Select adaptation, toast UI)
  • Page props typing
    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.
  • 服务端PRG与错误处理
    inertia-rails-controllers
    (redirect_back, to_hash, flash)
  • shadcn 输入组件
    shadcn-inertia
    (输入框/选择框适配、提示框UI)
  • 页面属性类型定义
    inertia-rails-typescript
    (使用
    type Props
    而非
    interface
    ,解决TS2344错误)
如果处理带图片预览、Active Storage或直接上传的文件上传场景,请务必完整阅读以下文档:
references/file-uploads.md
(约200行)—— 包含结合
<Form>
的图片预览、Active Storage集成、直接上传配置、多文件上传,以及进度追踪方案。