Tanstack Form

作者 tanstack-skills6f5521ecbdfb無授權條款35 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫8 個月前更新

Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, Lit, and Svelte.

AI 產生的概覽

TanStack Form 函式庫的參考指南,用於在 TypeScript 與 JavaScript 中建立型別安全的表單。

功能
提供 TanStack Form 的使用文件,這是一個無介面(headless)的表單狀態函式庫,支援 TS/JS 以及 React、Vue、Angular、Solid、Lit、Svelte 等框架。內容涵蓋安裝、useForm 鉤子、欄位元件、同步與非同步驗證、結構描述轉接器、陣列欄位、監聽器、反應式訂閱、表單與欄位狀態、FormApi 方法、共用設定、伺服器端驗證以及 TypeScript 型別。也列出最佳實務與常見陷阱。
適用情境
在實作或審查使用 TanStack Form 的表單,並需要關於驗證、陣列欄位、相依欄位或型別安全欄位路徑的指引時使用。適合在 React 或其他支援框架中工作、想查閱 API 與模式的開發者,而非可直接執行的工具。
執行需求
未包含指令碼,僅為文件。依照範例操作需要已安裝對應 TanStack Form 套件的 JavaScript 或 TypeScript 專案,並可選擇安裝 Zod 或 Valibot 轉接器。

Overview

TanStack Form is a headless form library with deep TypeScript integration. It provides field-level and form-level validation (sync/async), array fields, linked/dependent fields, fine-grained reactivity, and schema validation adapter support (Zod, Valibot, Yup).

Package: @tanstack/react-form Adapters: @tanstack/zod-form-adapter, @tanstack/valibot-form-adapter Status: Stable (v1)

Installation

bash
npm install @tanstack/react-form# Optional schema adapters:npm install @tanstack/zod-form-adapter zodnpm install @tanstack/valibot-form-adapter valibot

Core: useForm

tsx
import { useForm } from '@tanstack/react-form'
function MyForm() {  const form = useForm({    defaultValues: {      firstName: '',      lastName: '',      email: '',      age: 0,    },    onSubmit: async ({ value }) => {      // value is fully typed      await submitToServer(value)    },    onSubmitInvalid: ({ value, formApi }) => {      console.log('Validation failed:', formApi.state.errors)    },  })
  return (    <form      onSubmit={(e) => {        e.preventDefault()        e.stopPropagation()        form.handleSubmit()      }}    >      {/* Fields */}      <form.Subscribe        selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}        children={({ canSubmit, isSubmitting }) => (          <button type="submit" disabled={!canSubmit}>            {isSubmitting ? 'Submitting...' : 'Submit'}          </button>        )}      />    </form>  )}

Fields (form.Field)

tsx
<form.Field  name="firstName"  validators={{    onChange: ({ value }) =>      value.length < 3 ? 'Must be at least 3 characters' : undefined,  }}  children={(field) => (    <div>      <label htmlFor={field.name}>First Name</label>      <input        id={field.name}        name={field.name}        value={field.state.value}        onBlur={field.handleBlur}        onChange={(e) => field.handleChange(e.target.value)}      />      {field.state.meta.isTouched && field.state.meta.errors.length > 0 && (        <em>{field.state.meta.errors.join(', ')}</em>      )}    </div>  )}/>
<!-- Nested fields use dot notation --><form.Field name="address.city">  {(field) => (    <input      value={field.state.value}      onChange={(e) => field.handleChange(e.target.value)}      onBlur={field.handleBlur}    />  )}</form.Field>

Validation

Validation Timing

CauseWhen
onChangeAfter every value change
onBlurWhen field loses focus
onSubmitDuring submission
onMountWhen field mounts

Synchronous Validation

tsx
<form.Field  name="age"  validators={{    onChange: ({ value }) => {      if (value < 18) return 'Must be 18 or older'      return undefined // undefined = valid    },    onBlur: ({ value }) => {      if (!value) return 'Required'      return undefined    },  }}/>

Asynchronous Validation

tsx
<form.Field  name="username"  asyncDebounceMs={500}  validators={{    onChangeAsync: async ({ value }) => {      const res = await fetch(`/api/check-username?q=${value}`)      const { available } = await res.json()      if (!available) return 'Username taken'      return undefined    },  }}>  {(field) => (    <>      <input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />      {field.state.meta.isValidating && <span>Checking...</span>}    </>  )}</form.Field>

Schema Validation (Zod)

tsx
import { zodValidator } from '@tanstack/zod-form-adapter'import { z } from 'zod'
const form = useForm({  defaultValues: { email: '', age: 0 },  validatorAdapter: zodValidator(),  onSubmit: async ({ value }) => { /* ... */ },})
<form.Field  name="email"  validators={{    onChange: z.string().email('Invalid email'),    onBlur: z.string().min(1, 'Required'),  }}/>
<form.Field  name="age"  validators={{    onChange: z.number().min(18, 'Must be 18+'),  }}/>

Form-Level Validation

tsx
const form = useForm({  defaultValues: { password: '', confirmPassword: '' },  validators: {    onChange: ({ value }) => {      if (value.password !== value.confirmPassword) {        return 'Passwords do not match'      }      return undefined    },  },})

Linked/Dependent Fields

tsx
<form.Field  name="confirmPassword"  validators={{    onChangeListenTo: ['password'], // Re-validate when password changes    onChange: ({ value, fieldApi }) => {      const password = fieldApi.form.getFieldValue('password')      if (value !== password) return 'Passwords do not match'      return undefined    },  }}/>

Array Fields

tsx
<form.Field name="people" mode="array">  {(field) => (    <div>      {field.state.value.map((_, index) => (        <div key={index}>          <form.Field name={`people[${index}].name`}>            {(subField) => (              <input                value={subField.state.value}                onChange={(e) => subField.handleChange(e.target.value)}              />            )}          </form.Field>          <button type="button" onClick={() => field.removeValue(index)}>            Remove          </button>        </div>      ))}      <button type="button" onClick={() => field.pushValue({ name: '', age: 0 })}>        Add Person      </button>    </div>  )}</form.Field>

Array Methods

typescript
field.pushValue(item)              // Add to endfield.insertValue(index, item)     // Insert at indexfield.replaceValue(index, item)    // Replace at indexfield.removeValue(index)           // Remove at indexfield.swapValues(indexA, indexB)    // Swap positionsfield.moveValue(from, to)          // Move position

Listeners (Side Effects)

tsx
<form.Field  name="country"  listeners={{    onChange: ({ value }) => {      // Side effect: reset dependent fields      form.setFieldValue('state', '')      form.setFieldValue('postalCode', '')    },  }}/>

Reactivity (form.Subscribe & useStore)

tsx
// Render-prop subscription (fine-grained)<form.Subscribe  selector={(state) => ({ canSubmit: state.canSubmit, isDirty: state.isDirty })}  children={({ canSubmit, isDirty }) => (    <div>      {isDirty && <span>Unsaved changes</span>}      <button disabled={!canSubmit}>Save</button>    </div>  )}/>
// Hook-based subscriptionfunction FormStatus() {  const isValid = form.useStore((s) => s.isValid)  return isValid ? null : <p>Fix errors</p>}

Form State

typescript
interface FormState {  values: TFormData  errors: ValidationError[]  errorMap: Record<string, ValidationError>  isFormValid: boolean  isFieldsValid: boolean  isValid: boolean               // isFormValid && isFieldsValid  isTouched: boolean  isPristine: boolean  isDirty: boolean  isSubmitting: boolean  isSubmitted: boolean  isSubmitSuccessful: boolean  submissionAttempts: number  canSubmit: boolean             // isValid && !isSubmitting}

Field State

typescript
interface FieldState<TData> {  value: TData  meta: {    isTouched: boolean    isDirty: boolean    isPristine: boolean    isValidating: boolean    errors: ValidationError[]    errorMap: Record<ValidationCause, ValidationError>  }}

FormApi Methods

typescript
form.handleSubmit()form.reset()form.getFieldValue(field)form.setFieldValue(field, value)form.getFieldMeta(field)form.setFieldMeta(field, updater)form.validateAllFields(cause)form.validateField(field, cause)form.deleteField(field)

Shared Form Options (formOptions)

tsx
import { formOptions } from '@tanstack/react-form'
const sharedOpts = formOptions({  defaultValues: { firstName: '', lastName: '' },})
// Reuse across componentsconst form = useForm({  ...sharedOpts,  onSubmit: async ({ value }) => { /* ... */ },})

Server-Side Validation

tsx
// TanStack Start / Next.js server actionimport { ServerValidateError } from '@tanstack/react-form/nextjs'
export async function validateForm(data: FormData) {  const email = data.get('email') as string  if (await checkEmailExists(email)) {    throw new ServerValidateError({      form: 'Submission failed',      fields: { email: 'Email already registered' },    })  }}

TypeScript Integration

tsx
// Type-safe field paths with DeepKeysinterface UserForm {  name: string  address: { street: string; city: string }  tags: string[]  contacts: Array<{ name: string; phone: string }>}
// TypeScript auto-completes all valid paths:// 'name', 'address', 'address.street', 'address.city', 'tags', 'contacts'<form.Field name="address.city" />     // OK<form.Field name="nonexistent" />       // Type Error!

Best Practices

  1. Always call e.preventDefault() and e.stopPropagation() on form submit
  2. Always attach onBlur={field.handleBlur} for blur validation and isTouched tracking
  3. Use mode="array" for array fields to get array methods
  4. Return undefined (not null/false) for valid validators
  5. Use asyncDebounceMs for async validators to prevent API spam
  6. Check isTouched before showing errors for better UX
  7. Use form.Subscribe with selectors to minimize re-renders
  8. Use formOptions for shared configuration across components
  9. Use schema validators (Zod/Valibot) for complex validation rules
  10. Use onChangeListenTo for cross-field validation dependencies

Common Pitfalls

  • Forgetting e.preventDefault() on form submit (causes page reload)
  • Not attaching onBlur to inputs (breaks blur validation and isTouched)
  • Returning null or false instead of undefined for valid fields
  • Using mode="array" incorrectly (only needed on the array field itself, not sub-fields)
  • Subscribing to entire form state instead of using selectors (unnecessary re-renders)
  • Not using asyncDebounceMs with async validators (fires on every keystroke)

來源與署名

來源:tanstack-skills/tanstack-skills位於plugins/tanstack-form/skills/tanstack-form提交6f5521e

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架