tanstack-form

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TanStack 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-form
tsx
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-form
tsx
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
createFormHook
to pre-bind reusable UI components and reduce boilerplate:
tsx
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,
})
对于生产环境应用,使用
createFormHook
预绑定可复用UI组件,减少样板代码:
tsx
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-devtools
tsx
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-devtools
tsx
import { TanStackDevtools } from '@tanstack/react-devtools'
import { formDevtoolsPlugin } from '@tanstack/react-form-devtools'

<TanStackDevtools
  config={{ hideUntilHover: true }}
  plugins={[formDevtoolsPlugin()]}
/>

Rule Categories

规则分类

PriorityCategoryRule FileImpact
CRITICALForm Setup
rules/form-setup.md
Correct form creation and type inference
CRITICALValidation
rules/val-validation.md
Prevents invalid submissions and poor UX
CRITICALSchema Validation
rules/val-schema-validation.md
Type-safe validation with Zod/Valibot/ArkType
HIGHForm Composition
rules/comp-form-composition.md
Reduces boilerplate, enables reusable components
HIGHField State
rules/field-state.md
Correct state access and reactivity
HIGHArray Fields
rules/arr-array-fields.md
Dynamic list management
HIGHLinked Fields
rules/link-linked-fields.md
Cross-field validation (e.g. confirm password)
MEDIUMListeners
rules/listen-listeners.md
Side effects on field events
MEDIUMSubmission
rules/sub-submission.md
Correct submit handling and meta passing
MEDIUMSSR / Meta-Frameworks
rules/ssr-meta-frameworks.md
Server validation with Start/Next.js/Remix
LOWUI Libraries
rules/ui-libraries.md
Headless integration with component libraries
优先级分类规则文件影响
关键表单配置
rules/form-setup.md
正确创建表单及类型推导
关键校验
rules/val-validation.md
防止无效提交及糟糕的用户体验
关键Schema校验
rules/val-schema-validation.md
基于Zod/Valibot/ArkType的类型安全校验
表单组合
rules/comp-form-composition.md
减少样板代码,实现可复用组件
字段状态
rules/field-state.md
正确访问状态及响应式处理
数组字段
rules/arr-array-fields.md
动态列表管理
关联字段
rules/link-linked-fields.md
跨字段校验(如确认密码)
监听器
rules/listen-listeners.md
字段事件的副作用处理
提交处理
rules/sub-submission.md
正确处理提交及元数据传递
SSR/元框架
rules/ssr-meta-frameworks.md
与Start/Next.js/Remix集成的服务端校验
UI库集成
rules/ui-libraries.md
与组件库的无头集成

Critical Rules

核心规则

Always Do

务必遵循

  • Type from defaultValues — never pass generics to
    useForm<T>()
    , let TS infer from
    defaultValues
  • Prevent default on submit
    e.preventDefault(); e.stopPropagation(); form.handleSubmit()
  • Use
    children
    render prop
    form.Field
    uses render props via
    children={(field) => ...}
  • Use
    form.Subscribe
    with selector
    — subscribe to specific state slices to avoid re-renders
  • Use
    useStore
    with selector
    useStore(form.store, (s) => s.values.name)
    not
    useStore(form.store)
  • Use
    createFormHook
    in production — pre-bind components for consistency and less boilerplate
  • Debounce async validators — set
    onChangeAsyncDebounceMs
    or
    asyncDebounceMs
  • 从defaultValues推导类型 — 永远不要给
    useForm<T>()
    传递泛型,让TypeScript从
    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 — 预绑定组件以保证一致性并减少样板代码
  • 防抖异步校验器 — 设置
    onChangeAsyncDebounceMs
    asyncDebounceMs

Never Do

切勿操作

  • Pass generics
    useForm<MyType>()
    breaks the design; use typed
    defaultValues
    instead
  • Skip
    e.preventDefault()
    — native form submission will bypass TanStack Form's handling
  • Use
    useField
    for reactivity
    — use
    useStore(form.store)
    or
    form.Subscribe
    instead
  • Omit selector in
    useStore
    — causes full re-render on every state change
  • Use
    type="reset"
    without
    e.preventDefault()
    — native reset bypasses TanStack Form; use
    form.reset()
    explicitly
  • Expect transformed values in
    onSubmit
    — Standard Schema transforms aren't applied; parse manually in
    onSubmit
  • 传递泛型
    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" />} />
  },
})