TanStack Form 基础

要点

表单同时管理输入值、是否触碰、校验结果与提交状态。显示错误的时机与校验规则是两个决定。

为什么表单也需要 Headless

表单的难点从来不是 <input>,而是:脏检查、touched 状态、异步校验、提交中禁用、跨字段联动。TanStack Form 把这些状态机做成了 Headless API,输入框长什么样由你决定。

实践:注册表单

步骤一:useForm 声明表单

import { useForm } from '@tanstack/react-form'

type SignupValues = {
  username: string
  email: string
  age: number
}

function SignupForm() {
  const form = useForm({
    defaultValues: {
      username: '',
      email: '',
      age: 0,
    } satisfies SignupValues,
    onSubmit: async ({ value }) => {
      // value 的类型精确为 SignupValues
      await api.signup(value)
      console.log('提交成功', value)
    },
  })

  return null // 下一步填充 JSX
}

步骤二:字段绑定与渲染

<form
  onSubmit={(e) => {
    e.preventDefault()
    e.stopPropagation()
    form.handleSubmit()
  }}
>
  {/* name 必须是 defaultValues 中的键,写错直接编译报错 */}
  <form.Field
    name="username"
    validators={{
      // 函数校验器:返回错误字符串或 undefined
      onChange: ({ value }) =>
        value.length < 3 ? '用户名至少 3 个字符' : undefined,
    }}
  >
    {(field) => (
      <label>
        用户名
        <input
          name={field.name}
          value={field.state.value}
          onBlur={field.handleBlur}
          onChange={(e) => field.handleChange(e.target.value)}
        />
        {/* meta.errors 是数组;isValid 是现成的无错标志 */}
        {/* 元素可能是字符串(函数校验器)或带 message 的对象(Standard Schema),
            直接 String() 会把对象渲染成 [object Object] */}
        {!field.state.meta.isValid && (
          <em>{field.state.meta.errors
            .map((e) => (typeof e === 'string' ? e : e.message))
            .join(', ')}</em>
        )}
      </label>
    )}
  </form.Field>

  <form.Field name="email">
    {(field) => (
      <label>
        邮箱
        <input
          name={field.name}
          type="email"
          value={field.state.value}
          onBlur={field.handleBlur}
          onChange={(e) => field.handleChange(e.target.value)}
        />
      </label>
    )}
  </form.Field>

  <button type="submit">注册</button>
</form>

关键点解析:

  • field.state.value 读值、field.handleChange 写值,二者配合实现受控组件
  • field.handleBlur 触发 touched 标记,让 onBlur 校验成为可能
  • field.state.meta.errors 是数组;函数校验器返回字符串数组,Standard Schema 返回带 message 的对象数组——渲染前按 typeof 分派取文案,不要 String() 整个数组

步骤三:表单级校验(Standard Schema)

TanStack Form v1 原生支持 Standard Schema,Zod/Valibot/ArkType 的 schema 可以直接传入:

import { z } from 'zod'

const schema = z.object({
  username: z.string().min(3, '用户名至少 3 个字符'),
  email: z.string().email('邮箱格式不正确'),
  age: z.number().min(18, '需年满 18 岁'),
})

const form = useForm({
  defaultValues: { username: '', email: '', age: 0 },
  validators: {
    // 提交时整表校验,错误自动分发到对应字段
    onSubmit: schema,
  },
  onSubmit: async ({ value }) => {
    await api.signup(value)
  },
})

步骤四:Subscribe 读取派生状态

<form.Subscribe selector={(state) => state.canSubmit}>
  {(canSubmit) => (
    <button type="submit" disabled={!canSubmit}>
      注册
    </button>
  )}
</form.Subscribe>

selector 精确订阅所需状态,避免整表重渲染。常用状态:canSubmit、isSubmitting、isDirty、errors。

完整组件骨架

上面四个步骤合起来才是可运行文件。把它们拼进同一个组件(字段级校验、Schema 表单校验与 Subscribe 订阅可以共存):

import { useForm } from '@tanstack/react-form'
import { z } from 'zod'

const schema = z.object({
  username: z.string().min(3, '用户名至少 3 个字符'),
  email: z.string().email('邮箱格式不正确'),
  age: z.number().min(18, '需年满 18 岁'),
})

export function SignupForm() {
  const form = useForm({
    defaultValues: { username: '', email: '', age: 0 },
    validators: { onSubmit: schema },
    onSubmit: async ({ value }) => {
      await api.signup(value)
    },
  })

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        e.stopPropagation()
        form.handleSubmit()
      }}
    >
      <form.Field
        name="username"
        validators={{
          onChange: ({ value }) =>
            value.length < 3 ? '用户名至少 3 个字符' : undefined,
        }}
      >
        {(field) => (
          <label>
            用户名
            <input
              name={field.name}
              value={field.state.value}
              onBlur={field.handleBlur}
              onChange={(e) => field.handleChange(e.target.value)}
            />
            {!field.state.meta.isValid && (
              <em>{field.state.meta.errors
                .map((e) => (typeof e === 'string' ? e : e.message))
                .join(', ')}</em>
            )}
          </label>
        )}
      </form.Field>

      <form.Field name="email">
        {(field) => (
          <label>
            邮箱
            <input
              name={field.name}
              type="email"
              value={field.state.value}
              onBlur={field.handleBlur}
              onChange={(e) => field.handleChange(e.target.value)}
            />
          </label>
        )}
      </form.Field>

      <form.Subscribe selector={(state) => state.canSubmit}>
        {(canSubmit) => (
          <button type="submit" disabled={!canSubmit}>注册</button>
        )}
      </form.Subscribe>
    </form>
  )
}

最佳实践

字段校验适合及时提示“这个邮箱格式不对”,表单校验适合检查“结束时间早于开始时间”等字段关系;服务端仍须重新校验,客户端反馈不能成为信任边界。同步与异步的区别取决于是否要等待外部结果,而不是规则看起来复杂与否。

检查用户名是否占用时,应防抖并处理旧响应晚于新响应的情况:连续输入 A、B,即使 A 最后返回,也不能覆盖 B 的提示。输入组件通过库提供的订阅接口观察字段变化,避免只读取一个不会触发更新的状态快照。


练习与实践

练习一:最小表单

  • [ ] 实现用户名/邮箱两个字段:字段级校验即时提示,提交时打印 value
  • [ ] 给提交按钮加 isSubmitting 状态防重复提交

练习二:Schema 收编

  • [ ] 把练习一的字段校验全部迁移到 Zod schema(onSubmit + onChange 两个时机)
  • [ ] 用 form.Subscribe 实现”校验未通过时按钮禁用”(提示:state.errors 只含表单级错误、不含字段级错误,判断可提交应订阅 state.canSubmit)


延伸阅读

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇