web-forms-tanstack-form
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTanStack Form Patterns
TanStack Form 模式
Quick Guide: UsewithuseFormand typed generics. Render fields withdefaultValuesusing the render-propform.Fieldpattern. Validation lives in thechildrenprop on both form and field level — usevalidators,onChange,onBlur(sync) and theironSubmitvariants. UseAsyncfor dynamic field lists withmode="array"/pushValue. UseremoveValuefor cross-field validation. For app-wide consistency, create a sharedonChangeListenToviauseAppForm. Always providecreateFormHook— TanStack Form infers types from them.defaultValues
<critical_requirements>
快速指南: 将与useForm和类型化泛型配合使用。通过defaultValues采用渲染属性form.Field模式渲染字段。验证逻辑位于表单和字段级别的children属性中——使用validators、onChange、onBlur(同步)及其onSubmit变体。对动态字段列表使用Async,搭配mode="array"/pushValue。使用removeValue实现跨字段验证。为了保持应用全局一致性,通过onChangeListenTo创建共享的createFormHook。务必提供useAppForm——TanStack Form 会从中推断类型。defaultValues
<critical_requirements>
CRITICAL: Before Using This Skill
重要提示:使用此技能前须知
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,, named constants)import type
(You MUST provide to — TanStack Form infers field types from them)
defaultValuesuseForm(You MUST use with the render prop — TanStack Form does not use or )
form.FieldchildrenregisterController(You MUST use the prop for validation — NOT inline or external resolver wrappers)
validatorsrules(You MUST handle as an array — always over errors)
field.state.meta.errors.map()(You MUST call inside the form's handler with )
form.handleSubmit()onSubmite.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
(必须为 提供 ——TanStack Form 会从中推断字段类型)
useFormdefaultValues(必须使用带 渲染属性的 ——TanStack Form 不使用 或 )
childrenform.FieldregisterController(必须使用 属性进行验证——不能使用内联 或外部解析器包装器)
validatorsrules(必须将 作为数组处理——始终通过 遍历错误)
field.state.meta.errors.map()(必须在表单的 处理函数中调用 ,并搭配 )
onSubmitform.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 through every field name, value, and error — no manual generics required (though you can provide them).
defaultValuesCore Principles:
- Type inference from defaults - defines the form shape; field names and values are fully typed
defaultValues - Headless - Zero UI opinions; works with any component library or native inputs
- Validation-event-driven - Validators attach to specific events (,
onChange,onBlur) per field or per formonSubmit - Framework-agnostic core - Same mental model across React, Vue, Solid, Angular, and Lit
- Composition via factory - shares field/form components across an app
createFormHook
<patterns>
TanStack Form 本质是无头式且类型安全的。它不包含任何 UI——所有输入都由你自行渲染。该库提供表单状态、验证编排和字段管理功能。类型从 贯穿每个字段名称、值和错误——无需手动定义泛型(不过你也可以提供)。
defaultValues核心原则:
- 从默认值推断类型 - 定义表单结构;字段名称和值完全类型化
defaultValues - 无头式 - 无 UI 偏见;可与任何组件库或原生输入配合使用
- 验证事件驱动 - 验证器附加到字段或表单的特定事件(、
onChange、onBlur)onSubmit - 框架无关核心 - 在 React、Vue、Solid、Angular 和 Lit 中遵循相同的思维模型
- 通过工厂模式组合 - 在应用中共享字段/表单组件
createFormHook
<patterns>
Core Patterns
核心模式
Pattern 1: Basic useForm + form.Field
模式 1:基础 useForm + form.Field
Every form starts with and renders fields via . The render prop receives the field API with , , and .
useFormform.FieldchildrenstatehandleChangehandleBlurtsx
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 , no , no forwarding. You always use and explicitly.
registerControllerreffield.handleChangefield.state.valueSee examples/core.md for complete form with error display and accessibility.
每个表单都从 开始,并通过 渲染字段。 渲染属性会接收包含 、 和 的字段 API。
useFormform.FieldchildrenstatehandleChangehandleBlurtsx
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>
);与其他表单库的关键区别: 没有 ,没有 ,没有 转发。你始终需要显式使用 和 。
registerControllerreffield.handleChangefield.state.value查看 examples/core.md 获取包含错误展示和无障碍支持的完整表单示例。
Pattern 2: Field-Level Validation
模式 2:字段级验证
Validators are functions on the prop. Sync validators return a string (error) or (valid). Async validators use , , .
validatorsundefinedonChangeAsynconBlurAsynconSubmitAsynctsx
<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 and exist, the async validator only runs if the sync validator passes. Same for /.
onBluronBlurAsynconChangeonChangeAsyncSee examples/validation.md for all validation patterns and adapter integration.
验证器是 属性上的函数。同步验证器返回字符串(错误信息)或 (验证通过)。异步验证器使用 、、。
validatorsundefinedonChangeAsynconBlurAsynconSubmitAsynctsx
<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>
)}
/>同步优先机制: 当同时存在 和 时,只有同步验证器通过后,异步验证器才会运行。/ 同理。
onBluronBlurAsynconChangeonChangeAsync查看 examples/validation.md 获取所有验证模式和适配器集成示例。
Pattern 3: Linked Fields (Cross-Field Validation)
模式 3:关联字段(跨字段验证)
Use to re-run a field's validator when another field changes. This solves the stale-validation problem (e.g., confirm password).
onChangeListenTotsx
<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 , changing the field does not re-validate . The error stays stale until the user interacts with the confirm field again.
onChangeListenTopasswordconfirm_passwordSee examples/validation.md Pattern 4 for a complete linked fields example.
使用 在其他字段变化时重新运行当前字段的验证器。这解决了验证过时的问题(例如确认密码)。
onChangeListenTotsx
<form.Field
name="confirm_password"
validators={{
onChangeListenTo: ["password"],
onChange: ({ value, fieldApi }) => {
if (value !== fieldApi.form.getFieldValue("password")) {
return "密码不匹配";
}
return undefined;
},
}}
children={(field) => (/* ... */)}
/>重要性: 如果没有 ,修改 字段不会重新验证 ,错误信息会一直保持过时状态,直到用户再次与确认字段交互。
onChangeListenTopasswordconfirm_password查看 examples/validation.md 模式 4 获取完整的关联字段示例。
Pattern 4: Array Fields
模式 4:数组字段
Use on to get , , , , and for dynamic field groups.
mode="array"form.FieldpushValueremoveValueswapValuesmoveValueinsertValuetsx
<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: requires a complete object matching the array item shape. Partial objects will cause type errors.
pushValueSee examples/arrays.md for a complete dynamic list form.
在 上使用 ,以获取 、、、 和 来处理动态字段组。
form.Fieldmode="array"pushValueremoveValueswapValuesmoveValueinsertValuetsx
<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 apply to the entire form. Use for server-side validation that returns field-specific errors.
useFormonSubmitAsynctsx
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: — the key is optional for form-level errors, maps field names to their error messages. Return when valid.
{ form?: string, fields: Record<string, string> }formfieldsnullSee examples/validation.md Pattern 3 for complete form-level validation.
useFormonSubmitAsynctsx
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> }formfieldsnull查看 examples/validation.md 模式 3 获取完整的表单级验证示例。
Pattern 6: createFormHook (App-Wide Composition)
模式 6:createFormHook(应用全局组合)
Use to share custom field components and form components across the app. This eliminates boilerplate and enforces consistency.
createFormHooktsx
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: accepts all options. Registered and are available on the returned form instance: for custom field components, for form-level components.
useAppFormuseFormfieldComponentsformComponentsform.AppFieldform.AppFormSee examples/composition.md for the full factory setup and custom component patterns.
使用 在应用中共享自定义字段组件和表单组件。这能消除重复代码并确保一致性。
createFormHooktsx
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,
},
});用法: 接受所有 的配置项。已注册的 和 可在返回的表单实例上使用: 用于自定义字段组件, 用于表单级组件。
useAppFormuseFormfieldComponentsformComponentsform.AppFieldform.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 prop on .
listenersform.Fieldtsx
<form.Field
name="country"
listeners={{
onChange: ({ value }) => {
form.setFieldValue("province", "");
},
}}
children={(field) => (/* ... */)}
/>Available events: , , , . Listeners are for side effects only — they do not return validation errors.
onChangeonBluronMountonSubmitSee examples/composition.md Pattern 3 for a complete country/province cascade.
监听器响应字段事件并执行副作用,例如重置相关字段。使用 上的 属性。
form.Fieldlistenerstsx
<form.Field
name="country"
listeners={{
onChange: ({ value }) => {
form.setFieldValue("province", "");
},
}}
children={(field) => (/* ... */)}
/>可用事件: 、、、。监听器仅用于执行副作用——不返回验证错误。
onChangeonBluronMountonSubmit查看 examples/composition.md 模式 3 获取完整的国家/省份级联示例。
Pattern 8: form.Subscribe for Reactive UI
模式 8:form.Subscribe 用于响应式 UI
Use to reactively render UI based on form state without re-rendering the entire form. Takes a to pick specific state.
form.Subscribeselectortsx
<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 , reading directly causes the parent component to re-render on every state change. The selector narrows the subscription.
</patterns>
form.Subscribeform.state<red_flags>
使用 根据表单状态响应式渲染 UI,无需重新渲染整个表单。通过 选择特定状态。
form.Subscribeselectortsx
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit || isSubmitting}>
{isSubmitting ? "提交中..." : "提交"}
</button>
)}
/>重要性: 如果没有 ,直接读取 会导致父组件在每次状态变化时重新渲染。选择器能缩小订阅范围。
</patterns>
form.Subscribeform.state<red_flags>
RED FLAGS
注意事项
High Priority Issues:
- Using or
registerpatterns — TanStack Form usesControllerwithform.Fieldrender prop, not register/Controllerchildren - Missing in
defaultValues— types cannot be inferred, fields start asuseFormundefined - Calling without
form.handleSubmit()— causes page reloade.preventDefault() - Reading directly in the component body — causes full re-render on every change; use
form.stateorform.SubscribeuseStore
Medium Priority Issues:
- Using validator for expensive checks — use
onChangewith debounce oronChangeAsyncinsteadonBlurAsync - Providing partial objects to in array fields — must provide complete objects matching the array item type
pushValue - Not using for cross-field validation — related field errors go stale
onChangeListenTo - Wrapping in another async function without error handling —
form.handleSubmit()does not catch errors thrown inhandleSubmitonSubmit
Gotchas & Edge Cases:
- is always an array — never compare with
field.state.meta.errors, always===or.map().length - Sync validators gate async validators — if fails,
onChangedoes not runonChangeAsync - Form-level validator returns
onSubmitAsync— not the same shape as field-level validators{ fields: { fieldName: "error" } } - only becomes
field.state.meta.isTouchedaftertruefires — not on firsthandleBlurhandleChange - Array field access uses bracket notation: items[${i}].name
name={— not dot notation like}items.${i}.name - uses a
form.Subscribeprop to pick state — passing no selector subscribes to everythingselector - components are available as
createFormHookandform.AppField— not onform.AppFormform.Field
</red_flags>
<critical_reminders>
高优先级问题:
- 使用 或
register模式——TanStack Form 使用带Controller渲染属性的children,而非 register/Controllerform.Field - 中缺少
useForm——无法推断类型,字段初始值为defaultValuesundefined - 调用 时未使用
form.handleSubmit()——会导致页面刷新e.preventDefault() - 在组件主体中直接读取 ——每次变化都会导致完整重渲染;应使用
form.state或form.SubscribeuseStore
中优先级问题:
- 对耗时检查使用 验证器——应改用带防抖的
onChange或onChangeAsynconBlurAsync - 向数组字段的 传入部分对象——必须传入与数组项类型完全匹配的完整对象
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 - 数组字段访问使用方括号表示法:items[${i}].name
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 to — TanStack Form infers field types from them)
defaultValuesuseForm(You MUST use with the render prop — TanStack Form does not use or )
form.FieldchildrenregisterController(You MUST use the prop for validation — NOT inline or external resolver wrappers)
validatorsrules(You MUST handle as an array — always over errors)
field.state.meta.errors.map()(You MUST call inside the form's handler with )
form.handleSubmit()onSubmite.preventDefault()Failure to follow these rules will break form state, lose type safety, and produce incorrect validation behavior.
</critical_reminders>
所有代码必须遵循 CLAUDE.md 中的项目约定
(必须为 提供 ——TanStack Form 会从中推断字段类型)
useFormdefaultValues(必须使用带 渲染属性的 ——TanStack Form 不使用 或 )
childrenform.FieldregisterController(必须使用 属性进行验证——不能使用内联 或外部解析器包装器)
validatorsrules(必须将 作为数组处理——始终通过 遍历错误)
field.state.meta.errors.map()(必须在表单的 处理函数中调用 ,并搭配 )
onSubmitform.handleSubmit()e.preventDefault()不遵循这些规则会破坏表单状态、丢失类型安全并导致验证行为异常。
</critical_reminders>