tanstack-form
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTanStack Form
TanStack Form
Version: @tanstack/react-form@latest
Requires: React 18.0+, TypeScript 5.0+
版本:@tanstack/react-form@latest
依赖要求:React 18.0+、TypeScript 5.0+
Quick Setup
快速开始
bash
npm install @tanstack/react-formtsx
import { useForm } from '@tanstack/react-form'
function App() {
const form = useForm({
defaultValues: {
firstName: '',
lastName: '',
},
onSubmit: async ({ value }) => {
console.log(value)
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
<form.Field
name="firstName"
validators={{
onChange: ({ value }) =>
!value ? 'Required' : value.length < 3 ? 'Too short' : undefined,
}}
children={(field) => (
<>
<input
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{!field.state.meta.isValid && (
<em>{field.state.meta.errors.join(', ')}</em>
)}
</>
)}
/>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
)}
/>
</form>
)
}bash
npm install @tanstack/react-formtsx
import { useForm } from '@tanstack/react-form'
function App() {
const form = useForm({
defaultValues: {
firstName: '',
lastName: '',
},
onSubmit: async ({ value }) => {
console.log(value)
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
<form.Field
name="firstName"
validators={{
onChange: ({ value }) =>
!value ? 'Required' : value.length < 3 ? 'Too short' : undefined,
}}
children={(field) => (
<>
<input
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{!field.state.meta.isValid && (
<em>{field.state.meta.errors.join(', ')}</em>
)}
</>
)}
/>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
)}
/>
</form>
)
}Production Setup (Recommended)
生产环境配置(推荐)
For production apps, use to pre-bind reusable UI components and reduce boilerplate:
createFormHooktsx
import { createFormHookContexts, createFormHook } from '@tanstack/react-form'
import { TextField, NumberField, SubmitButton } from '~/ui-library'
const { fieldContext, formContext } = createFormHookContexts()
export const { useAppForm } = createFormHook({
fieldComponents: { TextField, NumberField },
formComponents: { SubmitButton },
fieldContext,
formContext,
})对于生产环境应用,使用预绑定可复用UI组件,减少样板代码:
createFormHooktsx
import { createFormHookContexts, createFormHook } from '@tanstack/react-form'
import { TextField, NumberField, SubmitButton } from '~/ui-library'
const { fieldContext, formContext } = createFormHookContexts()
export const { useAppForm } = createFormHook({
fieldComponents: { TextField, NumberField },
formComponents: { SubmitButton },
fieldContext,
formContext,
})Devtools
开发工具
bash
npm install -D @tanstack/react-devtools @tanstack/react-form-devtoolstsx
import { TanStackDevtools } from '@tanstack/react-devtools'
import { formDevtoolsPlugin } from '@tanstack/react-form-devtools'
<TanStackDevtools
config={{ hideUntilHover: true }}
plugins={[formDevtoolsPlugin()]}
/>bash
npm install -D @tanstack/react-devtools @tanstack/react-form-devtoolstsx
import { TanStackDevtools } from '@tanstack/react-devtools'
import { formDevtoolsPlugin } from '@tanstack/react-form-devtools'
<TanStackDevtools
config={{ hideUntilHover: true }}
plugins={[formDevtoolsPlugin()]}
/>Rule Categories
规则分类
| Priority | Category | Rule File | Impact |
|---|---|---|---|
| CRITICAL | Form Setup | | Correct form creation and type inference |
| CRITICAL | Validation | | Prevents invalid submissions and poor UX |
| CRITICAL | Schema Validation | | Type-safe validation with Zod/Valibot/ArkType |
| HIGH | Form Composition | | Reduces boilerplate, enables reusable components |
| HIGH | Field State | | Correct state access and reactivity |
| HIGH | Array Fields | | Dynamic list management |
| HIGH | Linked Fields | | Cross-field validation (e.g. confirm password) |
| MEDIUM | Listeners | | Side effects on field events |
| MEDIUM | Submission | | Correct submit handling and meta passing |
| MEDIUM | SSR / Meta-Frameworks | | Server validation with Start/Next.js/Remix |
| LOW | UI Libraries | | Headless integration with component libraries |
| 优先级 | 分类 | 规则文件 | 影响 |
|---|---|---|---|
| 关键 | 表单配置 | | 正确创建表单及类型推导 |
| 关键 | 校验 | | 防止无效提交及糟糕的用户体验 |
| 关键 | Schema校验 | | 基于Zod/Valibot/ArkType的类型安全校验 |
| 高 | 表单组合 | | 减少样板代码,实现可复用组件 |
| 高 | 字段状态 | | 正确访问状态及响应式处理 |
| 高 | 数组字段 | | 动态列表管理 |
| 高 | 关联字段 | | 跨字段校验(如确认密码) |
| 中 | 监听器 | | 字段事件的副作用处理 |
| 中 | 提交处理 | | 正确处理提交及元数据传递 |
| 中 | SSR/元框架 | | 与Start/Next.js/Remix集成的服务端校验 |
| 低 | UI库集成 | | 与组件库的无头集成 |
Critical Rules
核心规则
Always Do
务必遵循
- Type from defaultValues — never pass generics to , let TS infer from
useForm<T>()defaultValues - Prevent default on submit —
e.preventDefault(); e.stopPropagation(); form.handleSubmit() - Use render prop —
childrenuses render props viaform.Fieldchildren={(field) => ...} - Use with selector — subscribe to specific state slices to avoid re-renders
form.Subscribe - Use with selector —
useStorenotuseStore(form.store, (s) => s.values.name)useStore(form.store) - Use in production — pre-bind components for consistency and less boilerplate
createFormHook - Debounce async validators — set or
onChangeAsyncDebounceMsasyncDebounceMs
- 从defaultValues推导类型 — 永远不要给传递泛型,让TypeScript从
useForm<T>()自动推导defaultValues - 提交时阻止默认行为 —
e.preventDefault(); e.stopPropagation(); form.handleSubmit() - 使用children渲染属性 — 通过
form.Field使用渲染属性children={(field) => ...} - 结合selector使用form.Subscribe — 订阅特定状态片段以避免不必要的重渲染
- 结合selector使用useStore — 使用而非
useStore(form.store, (s) => s.values.name)useStore(form.store) - 生产环境使用createFormHook — 预绑定组件以保证一致性并减少样板代码
- 防抖异步校验器 — 设置或
onChangeAsyncDebounceMsasyncDebounceMs
Never Do
切勿操作
- Pass generics — breaks the design; use typed
useForm<MyType>()insteaddefaultValues - Skip — native form submission will bypass TanStack Form's handling
e.preventDefault() - Use for reactivity — use
useFieldoruseStore(form.store)insteadform.Subscribe - Omit selector in — causes full re-render on every state change
useStore - Use without
type="reset"— native reset bypasses TanStack Form; usee.preventDefault()explicitlyform.reset() - Expect transformed values in — Standard Schema transforms aren't applied; parse manually in
onSubmitonSubmit
- 传递泛型 — 会破坏设计;应使用带类型的
useForm<MyType>()替代defaultValues - 跳过e.preventDefault() — 原生表单提交会绕过TanStack Form的处理逻辑
- 使用useField实现响应式 — 应使用或
useStore(form.store)替代form.Subscribe - 在useStore中省略selector — 会导致每次状态变化都触发完整重渲染
- 未阻止默认行为就使用type="reset" — 原生重置会绕过TanStack Form;应显式使用
form.reset() - 期望onSubmit中获取转换后的值 — 标准Schema转换不会自动应用;需在中手动解析
onSubmit
Key Patterns
核心模式
tsx
// Schema validation (form-level with Zod)
const form = useForm({
defaultValues: { age: 0, name: '' },
validators: {
onChange: z.object({ age: z.number().min(13), name: z.string().min(1) }),
},
onSubmit: ({ value }) => console.log(value),
})
// Array fields
<form.Field name="hobbies" mode="array" children={(field) => (
<div>
{field.state.value.map((_, i) => (
<form.Field key={i} name={`hobbies[${i}].name`} children={(sub) => (
<input value={sub.state.value} onChange={(e) => sub.handleChange(e.target.value)} />
)} />
))}
<button type="button" onClick={() => field.pushValue({ name: '' })}>Add</button>
</div>
)} />
// Linked fields (confirm password)
<form.Field name="confirm_password" validators={{
onChangeListenTo: ['password'],
onChange: ({ value, fieldApi }) =>
value !== fieldApi.form.getFieldValue('password') ? 'Passwords do not match' : undefined,
}} children={(field) => <input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />} />
// Listeners (reset province when country changes)
<form.Field name="country" listeners={{
onChange: ({ value }) => { form.setFieldValue('province', '') },
}} children={(field) => <input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />} />
// Form composition with withForm
const ChildForm = withForm({
defaultValues: { firstName: '', lastName: '' },
render: function Render({ form }) {
return <form.AppField name="firstName" children={(field) => <field.TextField label="First Name" />} />
},
})tsx
// Schema校验(表单级别,基于Zod)
const form = useForm({
defaultValues: { age: 0, name: '' },
validators: {
onChange: z.object({ age: z.number().min(13), name: z.string().min(1) }),
},
onSubmit: ({ value }) => console.log(value),
})
// 数组字段
<form.Field name="hobbies" mode="array" children={(field) => (
<div>
{field.state.value.map((_, i) => (
<form.Field key={i} name={`hobbies[${i}].name`} children={(sub) => (
<input value={sub.state.value} onChange={(e) => sub.handleChange(e.target.value)} />
)} />
))}
<button type="button" onClick={() => field.pushValue({ name: '' })}>Add</button>
</div>
)} />
// 关联字段(确认密码)
<form.Field name="confirm_password" validators={{
onChangeListenTo: ['password'],
onChange: ({ value, fieldApi }) =>
value !== fieldApi.form.getFieldValue('password') ? 'Passwords do not match' : undefined,
}} children={(field) => <input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />} />
// 监听器(切换国家时重置省份)
<form.Field name="country" listeners={{
onChange: ({ value }) => { form.setFieldValue('province', '') },
}} children={(field) => <input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />} />
// 使用withForm组合表单
const ChildForm = withForm({
defaultValues: { firstName: '', lastName: '' },
render: function Render({ form }) {
return <form.AppField name="firstName" children={(field) => <field.TextField label="First Name" />} />
},
})