web-forms-tanstack-form

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TanStack Form Patterns

TanStack Form 模式

Quick Guide: Use
useForm
with
defaultValues
and typed generics. Render fields with
form.Field
using the render-prop
children
pattern. Validation lives in the
validators
prop on both form and field level — use
onChange
,
onBlur
,
onSubmit
(sync) and their
Async
variants. Use
mode="array"
for dynamic field lists with
pushValue
/
removeValue
. Use
onChangeListenTo
for cross-field validation. For app-wide consistency, create a shared
useAppForm
via
createFormHook
. Always provide
defaultValues
— TanStack Form infers types from them.

<critical_requirements>
快速指南:
useForm
defaultValues
和类型化泛型配合使用。通过
form.Field
采用渲染属性
children
模式渲染字段。验证逻辑位于表单和字段级别的
validators
属性中——使用
onChange
onBlur
onSubmit
(同步)及其
Async
变体。对动态字段列表使用
mode="array"
,搭配
pushValue
/
removeValue
。使用
onChangeListenTo
实现跨字段验证。为了保持应用全局一致性,通过
createFormHook
创建共享的
useAppForm
。务必提供
defaultValues
——TanStack Form 会从中推断类型。

<critical_requirements>

CRITICAL: Before Using This Skill

重要提示:使用此技能前须知

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type
, named constants)
(You MUST provide
defaultValues
to
useForm
— TanStack Form infers field types from them)
(You MUST use
form.Field
with the
children
render prop — TanStack Form does not use
register
or
Controller
)
(You MUST use the
validators
prop for validation — NOT inline
rules
or external resolver wrappers)
(You MUST handle
field.state.meta.errors
as an array — always
.map()
over errors)
(You MUST call
form.handleSubmit()
inside the form's
onSubmit
handler with
e.preventDefault()
)
</critical_requirements>

Auto-detection: TanStack Form, @tanstack/react-form, @tanstack/vue-form, @tanstack/solid-form, @tanstack/angular-form, @tanstack/lit-form, useForm from tanstack, form.Field, createFormHook, createFormHookContexts, useAppForm, fieldContext, formContext, handleSubmit tanstack, pushValue, removeValue, onChangeListenTo, field.handleChange, field.handleBlur, field.state, formDevtoolsPlugin
When to use:
  • Building type-safe forms where field types are inferred from
    defaultValues
  • Managing complex validation with sync, async, and cross-field rules
  • Dynamic forms with add/remove field groups (array fields)
  • Multi-framework projects (React, Vue, Solid, Angular, Lit)
  • Projects already using the TanStack ecosystem
When NOT to use:
  • Single input without validation (use native state)
  • Server-only forms with server actions (use native form + action)
  • Read-only data display (not a form scenario)

所有代码必须遵循 CLAUDE.md 中的项目约定(短横线命名、命名导出、导入排序、
import type
、命名常量)
(必须为
useForm
提供
defaultValues
——TanStack Form 会从中推断字段类型)
(必须使用带
children
渲染属性的
form.Field
——TanStack Form 不使用
register
Controller
(必须使用
validators
属性进行验证——不能使用内联
rules
或外部解析器包装器)
(必须将
field.state.meta.errors
作为数组处理——始终通过
.map()
遍历错误)
(必须在表单的
onSubmit
处理函数中调用
form.handleSubmit()
,并搭配
e.preventDefault()
</critical_requirements>

自动检测: TanStack Form、@tanstack/react-form、@tanstack/vue-form、@tanstack/solid-form、@tanstack/angular-form、@tanstack/lit-form、tanstack 中的 useForm、form.Field、createFormHook、createFormHookContexts、useAppForm、fieldContext、formContext、tanstack 的 handleSubmit、pushValue、removeValue、onChangeListenTo、field.handleChange、field.handleBlur、field.state、formDevtoolsPlugin
适用场景:
  • 构建字段类型从
    defaultValues
    推断的类型安全表单
  • 管理包含同步、异步和跨字段规则的复杂验证
  • 包含添加/删除字段组的动态表单(数组字段)
  • 多框架项目(React、Vue、Solid、Angular、Lit)
  • 已使用 TanStack 生态的项目
不适用场景:
  • 无验证的单个输入(使用原生状态)
  • 带服务端动作的仅服务端表单(使用原生表单 + 动作)
  • 只读数据展示(非表单场景)

Table of Contents

目录

Detailed Resources:
  • examples/core.md - Basic form, Field component, TypeScript, form submission
  • examples/validation.md - Sync/async validation, validator adapters, form-level validation
  • examples/arrays.md - Dynamic array fields with pushValue/removeValue
  • examples/composition.md - createFormHook, useAppForm, listeners, side effects
  • reference.md - API tables, validator events, decision frameworks

<philosophy>
详细资源:
  • examples/core.md - 基础表单、Field 组件、TypeScript、表单提交
  • examples/validation.md - 同步/异步验证、验证器适配器、表单级验证
  • examples/arrays.md - 搭配 pushValue/removeValue 的动态数组字段
  • examples/composition.md - createFormHook、useAppForm、监听器、副作用
  • reference.md - API 表格、验证器事件、决策框架

<philosophy>

Philosophy

设计理念

TanStack Form is headless and type-safe by design. It owns zero UI — you render every input yourself. The library provides form state, validation orchestration, and field management. Types flow from
defaultValues
through every field name, value, and error — no manual generics required (though you can provide them).
Core Principles:
  1. Type inference from defaults -
    defaultValues
    defines the form shape; field names and values are fully typed
  2. Headless - Zero UI opinions; works with any component library or native inputs
  3. Validation-event-driven - Validators attach to specific events (
    onChange
    ,
    onBlur
    ,
    onSubmit
    ) per field or per form
  4. Framework-agnostic core - Same mental model across React, Vue, Solid, Angular, and Lit
  5. Composition via factory -
    createFormHook
    shares field/form components across an app
</philosophy>
<patterns>
TanStack Form 本质是无头式且类型安全的。它不包含任何 UI——所有输入都由你自行渲染。该库提供表单状态、验证编排和字段管理功能。类型从
defaultValues
贯穿每个字段名称、值和错误——无需手动定义泛型(不过你也可以提供)。
核心原则:
  1. 从默认值推断类型 -
    defaultValues
    定义表单结构;字段名称和值完全类型化
  2. 无头式 - 无 UI 偏见;可与任何组件库或原生输入配合使用
  3. 验证事件驱动 - 验证器附加到字段或表单的特定事件(
    onChange
    onBlur
    onSubmit
  4. 框架无关核心 - 在 React、Vue、Solid、Angular 和 Lit 中遵循相同的思维模型
  5. 通过工厂模式组合 -
    createFormHook
    在应用中共享字段/表单组件
</philosophy>
<patterns>

Core Patterns

核心模式

Pattern 1: Basic useForm + form.Field

模式 1:基础 useForm + form.Field

Every form starts with
useForm
and renders fields via
form.Field
. The
children
render prop receives the field API with
state
,
handleChange
, and
handleBlur
.
tsx
import { useForm } from "@tanstack/react-form";

const form = useForm({
  defaultValues: { name: "", email: "" },
  onSubmit: async ({ value }) => {
    await submitToApi(value);
  },
});

return (
  <form
    onSubmit={(e) => {
      e.preventDefault();
      form.handleSubmit();
    }}
  >
    <form.Field
      name="email"
      children={(field) => (
        <input
          value={field.state.value}
          onBlur={field.handleBlur}
          onChange={(e) => field.handleChange(e.target.value)}
        />
      )}
    />
  </form>
);
Key difference from other form libraries: No
register
, no
Controller
, no
ref
forwarding. You always use
field.handleChange
and
field.state.value
explicitly.
See examples/core.md for complete form with error display and accessibility.

每个表单都从
useForm
开始,并通过
form.Field
渲染字段。
children
渲染属性会接收包含
state
handleChange
handleBlur
的字段 API。
tsx
import { useForm } from "@tanstack/react-form";

const form = useForm({
  defaultValues: { name: "", email: "" },
  onSubmit: async ({ value }) => {
    await submitToApi(value);
  },
});

return (
  <form
    onSubmit={(e) => {
      e.preventDefault();
      form.handleSubmit();
    }}
  >
    <form.Field
      name="email"
      children={(field) => (
        <input
          value={field.state.value}
          onBlur={field.handleBlur}
          onChange={(e) => field.handleChange(e.target.value)}
        />
      )}
    />
  </form>
);
与其他表单库的关键区别: 没有
register
,没有
Controller
,没有
ref
转发。你始终需要显式使用
field.handleChange
field.state.value
查看 examples/core.md 获取包含错误展示和无障碍支持的完整表单示例。

Pattern 2: Field-Level Validation

模式 2:字段级验证

Validators are functions on the
validators
prop. Sync validators return a string (error) or
undefined
(valid). Async validators use
onChangeAsync
,
onBlurAsync
,
onSubmitAsync
.
tsx
<form.Field
  name="age"
  validators={{
    onChange: ({ value }) => (value < 13 ? "Must be 13 or older" : undefined),
    onBlurAsync: async ({ value }) => {
      const exists = await checkAge(value);
      return exists ? undefined : "Age not valid on server";
    },
  }}
  children={(field) => (
    <div>
      <input
        type="number"
        value={field.state.value}
        onBlur={field.handleBlur}
        onChange={(e) => field.handleChange(e.target.valueAsNumber)}
      />
      {field.state.meta.errors.map((err) => (
        <em key={err} role="alert">
          {err}
        </em>
      ))}
    </div>
  )}
/>
Sync-first gating: When both
onBlur
and
onBlurAsync
exist, the async validator only runs if the sync validator passes. Same for
onChange
/
onChangeAsync
.
See examples/validation.md for all validation patterns and adapter integration.

验证器是
validators
属性上的函数。同步验证器返回字符串(错误信息)或
undefined
(验证通过)。异步验证器使用
onChangeAsync
onBlurAsync
onSubmitAsync
tsx
<form.Field
  name="age"
  validators={{
    onChange: ({ value }) => (value < 13 ? "必须年满 13 岁" : undefined),
    onBlurAsync: async ({ value }) => {
      const exists = await checkAge(value);
      return exists ? undefined : "该年龄在服务端无效";
    },
  }}
  children={(field) => (
    <div>
      <input
        type="number"
        value={field.state.value}
        onBlur={field.handleBlur}
        onChange={(e) => field.handleChange(e.target.valueAsNumber)}
      />
      {field.state.meta.errors.map((err) => (
        <em key={err} role="alert">
          {err}
        </em>
      ))}
    </div>
  )}
/>
同步优先机制: 当同时存在
onBlur
onBlurAsync
时,只有同步验证器通过后,异步验证器才会运行。
onChange
/
onChangeAsync
同理。
查看 examples/validation.md 获取所有验证模式和适配器集成示例。

Pattern 3: Linked Fields (Cross-Field Validation)

模式 3:关联字段(跨字段验证)

Use
onChangeListenTo
to re-run a field's validator when another field changes. This solves the stale-validation problem (e.g., confirm password).
tsx
<form.Field
  name="confirm_password"
  validators={{
    onChangeListenTo: ["password"],
    onChange: ({ value, fieldApi }) => {
      if (value !== fieldApi.form.getFieldValue("password")) {
        return "Passwords do not match";
      }
      return undefined;
    },
  }}
  children={(field) => (/* ... */)}
/>
Why this matters: Without
onChangeListenTo
, changing the
password
field does not re-validate
confirm_password
. The error stays stale until the user interacts with the confirm field again.
See examples/validation.md Pattern 4 for a complete linked fields example.

使用
onChangeListenTo
在其他字段变化时重新运行当前字段的验证器。这解决了验证过时的问题(例如确认密码)。
tsx
<form.Field
  name="confirm_password"
  validators={{
    onChangeListenTo: ["password"],
    onChange: ({ value, fieldApi }) => {
      if (value !== fieldApi.form.getFieldValue("password")) {
        return "密码不匹配";
      }
      return undefined;
    },
  }}
  children={(field) => (/* ... */)}
/>
重要性: 如果没有
onChangeListenTo
,修改
password
字段不会重新验证
confirm_password
,错误信息会一直保持过时状态,直到用户再次与确认字段交互。
查看 examples/validation.md 模式 4 获取完整的关联字段示例。

Pattern 4: Array Fields

模式 4:数组字段

Use
mode="array"
on
form.Field
to get
pushValue
,
removeValue
,
swapValues
,
moveValue
, and
insertValue
for dynamic field groups.
tsx
<form.Field
  name="hobbies"
  mode="array"
  children={(hobbiesField) => (
    <div>
      {hobbiesField.state.value.map((_, i) => (
        <div key={i}>
          <form.Field
            name={`hobbies[${i}].name`}
            children={(field) => (
              <input
                value={field.state.value}
                onChange={(e) => field.handleChange(e.target.value)}
              />
            )}
          />
          <button type="button" onClick={() => hobbiesField.removeValue(i)}>
            Remove
          </button>
        </div>
      ))}
      <button
        type="button"
        onClick={() => hobbiesField.pushValue({ name: "" })}
      >
        Add hobby
      </button>
    </div>
  )}
/>
Important:
pushValue
requires a complete object matching the array item shape. Partial objects will cause type errors.
See examples/arrays.md for a complete dynamic list form.

form.Field
上使用
mode="array"
,以获取
pushValue
removeValue
swapValues
moveValue
insertValue
来处理动态字段组。
tsx
<form.Field
  name="hobbies"
  mode="array"
  children={(hobbiesField) => (
    <div>
      {hobbiesField.state.value.map((_, i) => (
        <div key={i}>
          <form.Field
            name={`hobbies[${i}].name`}
            children={(field) => (
              <input
                value={field.state.value}
                onChange={(e) => field.handleChange(e.target.value)}
              />
            )}
          />
          <button type="button" onClick={() => hobbiesField.removeValue(i)}>
            删除
          </button>
        </div>
      ))}
      <button
        type="button"
        onClick={() => hobbiesField.pushValue({ name: "" })}
      >
        添加爱好
      </button>
    </div>
  )}
/>
注意:
pushValue
需要传入与数组项结构完全匹配的完整对象。部分对象会导致类型错误。
查看 examples/arrays.md 获取完整的动态列表表单示例。

Pattern 5: Form-Level Validation

模式 5:表单级验证

Validators on
useForm
apply to the entire form. Use
onSubmitAsync
for server-side validation that returns field-specific errors.
tsx
const form = useForm({
  defaultValues: { username: "", age: 0 },
  validators: {
    onSubmitAsync: async ({ value }) => {
      const errors = await validateOnServer(value);
      if (errors) {
        return {
          form: "Submission failed",
          fields: {
            username: errors.username,
            age: errors.age,
          },
        };
      }
      return null;
    },
  },
});
Return shape:
{ form?: string, fields: Record<string, string> }
— the
form
key is optional for form-level errors,
fields
maps field names to their error messages. Return
null
when valid.
See examples/validation.md Pattern 3 for complete form-level validation.

useForm
上的验证器适用于整个表单。使用
onSubmitAsync
实现返回字段特定错误的服务端验证。
tsx
const form = useForm({
  defaultValues: { username: "", age: 0 },
  validators: {
    onSubmitAsync: async ({ value }) => {
      const errors = await validateOnServer(value);
      if (errors) {
        return {
          form: "提交失败",
          fields: {
            username: errors.username,
            age: errors.age,
          },
        };
      }
      return null;
    },
  },
});
返回结构:
{ form?: string, fields: Record<string, string> }
——
form
键是可选的表单级错误,
fields
映射字段名称到对应的错误信息。验证通过时返回
null
查看 examples/validation.md 模式 3 获取完整的表单级验证示例。

Pattern 6: createFormHook (App-Wide Composition)

模式 6:createFormHook(应用全局组合)

Use
createFormHook
to share custom field components and form components across the app. This eliminates boilerplate and enforces consistency.
tsx
import { createFormHookContexts, createFormHook } from "@tanstack/react-form";

export const { fieldContext, formContext, useFieldContext } =
  createFormHookContexts();

export const { useAppForm, withForm } = createFormHook({
  fieldContext,
  formContext,
  fieldComponents: {
    TextField: TextFieldComponent,
    SelectField: SelectFieldComponent,
  },
  formComponents: {
    SubmitButton: SubmitButtonComponent,
  },
});
Usage:
useAppForm
accepts all
useForm
options. Registered
fieldComponents
and
formComponents
are available on the returned form instance:
form.AppField
for custom field components,
form.AppForm
for form-level components.
See examples/composition.md for the full factory setup and custom component patterns.

使用
createFormHook
在应用中共享自定义字段组件和表单组件。这能消除重复代码并确保一致性。
tsx
import { createFormHookContexts, createFormHook } from "@tanstack/react-form";

export const { fieldContext, formContext, useFieldContext } =
  createFormHookContexts();

export const { useAppForm, withForm } = createFormHook({
  fieldContext,
  formContext,
  fieldComponents: {
    TextField: TextFieldComponent,
    SelectField: SelectFieldComponent,
  },
  formComponents: {
    SubmitButton: SubmitButtonComponent,
  },
});
用法:
useAppForm
接受所有
useForm
的配置项。已注册的
fieldComponents
formComponents
可在返回的表单实例上使用:
form.AppField
用于自定义字段组件,
form.AppForm
用于表单级组件。
查看 examples/composition.md 获取完整的工厂设置和自定义组件模式。

Pattern 7: Listeners (Side Effects)

模式 7:监听器(副作用)

Listeners react to field events and perform side effects like resetting related fields. Use the
listeners
prop on
form.Field
.
tsx
<form.Field
  name="country"
  listeners={{
    onChange: ({ value }) => {
      form.setFieldValue("province", "");
    },
  }}
  children={(field) => (/* ... */)}
/>
Available events:
onChange
,
onBlur
,
onMount
,
onSubmit
. Listeners are for side effects only — they do not return validation errors.
See examples/composition.md Pattern 3 for a complete country/province cascade.

监听器响应字段事件并执行副作用,例如重置相关字段。使用
form.Field
上的
listeners
属性。
tsx
<form.Field
  name="country"
  listeners={{
    onChange: ({ value }) => {
      form.setFieldValue("province", "");
    },
  }}
  children={(field) => (/* ... */)}
/>
可用事件:
onChange
onBlur
onMount
onSubmit
。监听器仅用于执行副作用——不返回验证错误。
查看 examples/composition.md 模式 3 获取完整的国家/省份级联示例。

Pattern 8: form.Subscribe for Reactive UI

模式 8:form.Subscribe 用于响应式 UI

Use
form.Subscribe
to reactively render UI based on form state without re-rendering the entire form. Takes a
selector
to pick specific state.
tsx
<form.Subscribe
  selector={(state) => [state.canSubmit, state.isSubmitting]}
  children={([canSubmit, isSubmitting]) => (
    <button type="submit" disabled={!canSubmit || isSubmitting}>
      {isSubmitting ? "Submitting..." : "Submit"}
    </button>
  )}
/>
Why this matters: Without
form.Subscribe
, reading
form.state
directly causes the parent component to re-render on every state change. The selector narrows the subscription.
</patterns>
<red_flags>
使用
form.Subscribe
根据表单状态响应式渲染 UI,无需重新渲染整个表单。通过
selector
选择特定状态。
tsx
<form.Subscribe
  selector={(state) => [state.canSubmit, state.isSubmitting]}
  children={([canSubmit, isSubmitting]) => (
    <button type="submit" disabled={!canSubmit || isSubmitting}>
      {isSubmitting ? "提交中..." : "提交"}
    </button>
  )}
/>
重要性: 如果没有
form.Subscribe
,直接读取
form.state
会导致父组件在每次状态变化时重新渲染。选择器能缩小订阅范围。
</patterns>
<red_flags>

RED FLAGS

注意事项

High Priority Issues:
  • Using
    register
    or
    Controller
    patterns — TanStack Form uses
    form.Field
    with
    children
    render prop, not register/Controller
  • Missing
    defaultValues
    in
    useForm
    — types cannot be inferred, fields start as
    undefined
  • Calling
    form.handleSubmit()
    without
    e.preventDefault()
    — causes page reload
  • Reading
    form.state
    directly in the component body — causes full re-render on every change; use
    form.Subscribe
    or
    useStore
Medium Priority Issues:
  • Using
    onChange
    validator for expensive checks — use
    onChangeAsync
    with debounce or
    onBlurAsync
    instead
  • Providing partial objects to
    pushValue
    in array fields — must provide complete objects matching the array item type
  • Not using
    onChangeListenTo
    for cross-field validation — related field errors go stale
  • Wrapping
    form.handleSubmit()
    in another async function without error handling —
    handleSubmit
    does not catch errors thrown in
    onSubmit
Gotchas & Edge Cases:
  • field.state.meta.errors
    is always an array — never compare with
    ===
    , always
    .map()
    or
    .length
  • Sync validators gate async validators — if
    onChange
    fails,
    onChangeAsync
    does not run
  • Form-level
    onSubmitAsync
    validator returns
    { fields: { fieldName: "error" } }
    — not the same shape as field-level validators
  • field.state.meta.isTouched
    only becomes
    true
    after
    handleBlur
    fires — not on first
    handleChange
  • Array field access uses bracket notation:
    name={
    items[${i}].name
    }
    — not dot notation like
    items.${i}.name
  • form.Subscribe
    uses a
    selector
    prop to pick state — passing no selector subscribes to everything
  • createFormHook
    components are available as
    form.AppField
    and
    form.AppForm
    — not on
    form.Field
</red_flags>

<critical_reminders>
高优先级问题:
  • 使用
    register
    Controller
    模式——TanStack Form 使用带
    children
    渲染属性的
    form.Field
    ,而非 register/Controller
  • useForm
    中缺少
    defaultValues
    ——无法推断类型,字段初始值为
    undefined
  • 调用
    form.handleSubmit()
    时未使用
    e.preventDefault()
    ——会导致页面刷新
  • 在组件主体中直接读取
    form.state
    ——每次变化都会导致完整重渲染;应使用
    form.Subscribe
    useStore
中优先级问题:
  • 对耗时检查使用
    onChange
    验证器——应改用带防抖的
    onChangeAsync
    onBlurAsync
  • 向数组字段的
    pushValue
    传入部分对象——必须传入与数组项类型完全匹配的完整对象
  • 跨字段验证未使用
    onChangeListenTo
    ——相关字段的错误会过时
  • 在未处理错误的情况下将
    form.handleSubmit()
    包装在另一个异步函数中——
    handleSubmit
    不会捕获
    onSubmit
    中抛出的错误
陷阱与边缘情况:
  • field.state.meta.errors
    始终是数组——永远不要用
    ===
    比较,应始终使用
    .map()
    .length
  • 同步验证器会拦截异步验证器——如果
    onChange
    失败,
    onChangeAsync
    不会运行
  • 表单级
    onSubmitAsync
    验证器返回
    { fields: { fieldName: "error" } }
    ——与字段级验证器的返回结构不同
  • field.state.meta.isTouched
    仅在
    handleBlur
    触发后才变为
    true
    ——并非首次
    handleChange
  • 数组字段访问使用方括号表示法:
    name={
    items[${i}].name
    }
    ——不能使用点表示法如
    items.${i}.name
  • form.Subscribe
    使用
    selector
    属性选择状态——不传入选择器会订阅所有状态
  • createFormHook
    组件通过
    form.AppField
    form.AppForm
    访问——不在
    form.Field
</red_flags>

<critical_reminders>

CRITICAL REMINDERS

重要提醒

All code must follow project conventions in CLAUDE.md
(You MUST provide
defaultValues
to
useForm
— TanStack Form infers field types from them)
(You MUST use
form.Field
with the
children
render prop — TanStack Form does not use
register
or
Controller
)
(You MUST use the
validators
prop for validation — NOT inline
rules
or external resolver wrappers)
(You MUST handle
field.state.meta.errors
as an array — always
.map()
over errors)
(You MUST call
form.handleSubmit()
inside the form's
onSubmit
handler with
e.preventDefault()
)
Failure to follow these rules will break form state, lose type safety, and produce incorrect validation behavior.
</critical_reminders>
所有代码必须遵循 CLAUDE.md 中的项目约定
(必须为
useForm
提供
defaultValues
——TanStack Form 会从中推断字段类型)
(必须使用带
children
渲染属性的
form.Field
——TanStack Form 不使用
register
Controller
(必须使用
validators
属性进行验证——不能使用内联
rules
或外部解析器包装器)
(必须将
field.state.meta.errors
作为数组处理——始终通过
.map()
遍历错误)
(必须在表单的
onSubmit
处理函数中调用
form.handleSubmit()
,并搭配
e.preventDefault()
不遵循这些规则会破坏表单状态、丢失类型安全并导致验证行为异常。
</critical_reminders>