TanStack Form React 快速上手:用 createFormHook 构建类型安全的表单状态管理
【免费下载链接】form🤖 Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, and Lit.项目地址: https://gitcode.com/GitHub_Trending/form/form
导读
本文以 docs/framework/react/quick-start.md 为核心指南,带你快速上手 TanStack Form 在 React 中的两种主流用法:面向长期可维护性的createFormHook组合式写法,以及适合一次性组件的useForm+form.Field写法。读完本文,你将掌握如何用约 60 行代码搭建一个具备完整类型推断、内置同步/异步校验与 Standard Schema(Zod 等)校验能力的生产级表单,并理解form.AppField、form.AppForm背后基于 Context 与 TanStack Store 的实现原理。
TanStack Form 与你之前用过的表单库不同,它面向大规模生产场景设计,把类型安全、性能与组合能力放在首位,因此官方围绕其使用方式沉淀了一套 使用哲学:重视可扩展性与长期开发体验,而非追求短小精悍的示例代码。下面给出的示例遵循了其中的绝大多数最佳实践,可以帮助你在短暂上手后快速开发出即使是高复杂度的表单。
方式一:使用 createFormHook 组合你的表单
这是官方推荐的生产级写法。核心思想是把"表单 UI 组件"与"表单状态逻辑"解耦:先用createFormHookContexts()创建字段/表单上下文,再通过createFormHook把你自己实现的TextField、NumberField、SubmitButton等组件预绑定到表单上,最终得到一个应用级的useAppFormHook。
完整示例
import React from 'react' import ReactDOM from 'react-dom/client' import { createFormHook, createFormHookContexts } from '@tanstack/react-form' // Form components that pre-bind events from the form hook; check our "Form Composition" guide for more import { TextField, NumberField, SubmitButton } from '~our-app/ui-library' // We also support Valibot, ArkType, and any other standard schema library import { z } from 'zod' const { fieldContext, formContext } = createFormHookContexts() // Allow us to bind components to the form to keep type safety but reduce production boilerplate // Define this once to have a generator of consistent form instances throughout your app const { useAppForm } = createFormHook({ fieldComponents: { TextField, NumberField, }, formComponents: { SubmitButton, }, fieldContext, formContext, }) const PeoplePage = () => { const form = useAppForm({ defaultValues: { username: '', age: 0, }, validators: { // Pass a schema or function to validate onChange: z.object({ username: z.string(), age: z.number().min(13), }), }, onSubmit: ({ value }) => { // Do something with form data alert(JSON.stringify(value, null, 2)) }, }) return ( <form onSubmit={(e) => { e.preventDefault() form.handleSubmit() }} > <h1>Personal Information</h1> {/* Components are bound to `form` and `field` to ensure extreme type safety */} {/* Use `form.AppField` to render a component bound to a single field */} <form.AppField name="username" children={(field) => <field.TextField label="Full Name" />} /> {/* The "name" property will throw a TypeScript error if typo'd */} <form.AppField name="age" children={(field) => <field.NumberField label="Age" />} /> {/* Components in `form.AppForm` have access to the form context */} <form.AppForm> <form.SubmitButton /> </form.AppForm> </form> ) } const rootElement = document.getElementById('root')! ReactDOM.createRoot(rootElement).render(<PeoplePage />)这段代码浓缩了 TanStack Form 的几个关键设计:
createFormHookContexts():创建一对fieldContext与formContext,用于在"字段组件/表单组件"与"form 实例"之间传递数据;createFormHook({ fieldComponents, formComponents, fieldContext, formContext }):把 UI 组件注册进表单,返回应用专属的useAppForm;该 Hook 与useForm接受完全相同的选项(defaultValues、validators、onSubmit等),但在其返回的 form 上额外挂载了AppField、AppForm与所有formComponents;form.AppField:渲染一个绑定到单个字段的组件,name拼错时 TypeScript 会直接报错;form.AppForm:为formComponents(如SubmitButton)提供 form 上下文,使其能通过useFormContext()读取表单状态(例如isSubmitting)。
源码视角:AppField 与 AppForm 是如何工作的
从 packages/react-form/src/createFormHook.tsx 的源码可以看到,createFormHook内部返回{ useAppForm, withForm, withFieldGroup, useTypedAppFormContext, extendForm },其中useAppForm通过如下方式扩展 form 实例:
- 它先调用
useForm(props)得到基础 form 实例(见 packages/react-form/src/useForm.tsx,内部通过useState创建FormApi实例并挂载); AppForm是一个用useMemo缓存的组件,内部渲染<formContext.Provider value={form}>{children}</formContext.Provider>;AppField内部渲染<form.Field {...props}>,再把 field 实例放进fieldContext.Provider,并通过Object.assign(field, fieldComponents)把注册的字段组件直接挂到 field 对象上——这正是children={(field) => <field.TextField .../>}能够工作的原因;- 最后
Object.assign(form, { AppField, AppForm, ...formComponents })把扩展 API 合并回 form 实例。
也就是说,form.AppField等价于"自带 Context 注入的form.Field",form.AppForm等价于"自带 form Context Provider 的容器"。这套机制保证:useFieldContext一定拿到的是当前字段的FieldApi,useFormContext一定拿到的是当前表单的FormApi,类型在整条链路上全程可推断。
这一组合能力的完整进阶用法(自定义 Hook、预绑定字段/表单组件、withForm、withFieldGroup、extendForm、按需懒加载组件等)可继续阅读 表单组合指南。
方式二:使用 useForm 与 form.Field 的一次性写法
虽然官方长期更建议使用createFormHook来减少样板代码,但库同样支持用useForm和form.Field编写一次性组件,适合学习、原型或不需要复用的场景:
import React from 'react' import ReactDOM from 'react-dom/client' import { useForm } from '@tanstack/react-form' const PeoplePage = () => { const form = useForm({ defaultValues: { username: '', age: 0, }, onSubmit: ({ value }) => { // Do something with form data alert(JSON.stringify(value, null, 2)) }, }) return ( <form.Field name="age" validators={{ // We can choose between form-wide and field-specific validators onChange: ({ value }) => value > 13 ? undefined : 'Must be 13 or older', }} children={(field) => ( <> <input name={field.name} value={field.state.value} onBlur={field.handleBlur} type="number" onChange={(e) => field.handleChange(e.target.valueAsNumber)} /> {!field.state.meta.isValid && ( <em>{field.state.meta.errors.join(',')}</em> )} </> )} /> ) } const rootElement = document.getElementById('root')! ReactDOM.createRoot(rootElement).render(<PeoplePage />)两种写法的关系
所有useForm的属性都可以用在useAppForm中,所有form.Field的属性都可以用在form.AppField中。也就是说,createFormHook不是另一套 API,而是对useForm体系的组合增强。
从源码上看,useForm返回的是一个ReactFormExtendedApi(见 packages/react-form/src/useForm.tsx),它在FormApi基础上补充了 React 专属成员:
Field:渲染单个字段的组件,内部基于useFieldHook 创建FieldApi(见 packages/react-form/src/useField.tsx),并通过useSelector(fieldApi.store, ...)精细订阅 value、isTouched、isBlurred、isDirty、errorMap、isValidating等响应式状态,避免无关重渲染;FormGroup:用于把一组字段组织成子表单;Subscribe:订阅 form 状态(如state.canSubmit、state.isSubmitting)的组件式 API,适合在 UI 中做局部响应。
在快速上手的示例中,field.state.value与field.state.meta.errors都是响应式读取的:errors是错误消息数组,可直接join(',')渲染;而field.state.meta.isValid为false表示当前字段校验未通过。
校验:从函数到 Standard Schema
快速上手示例中把 Zod schema 直接传给了validators.onChange:
validators: { onChange: z.object({ username: z.string(), age: z.number().min(13), }), },TanStack Form 原生支持所有遵循 Standard Schema 规范 的校验库,包括 Zod、Valibot、ArkType、Yup 等。你既可以像上面这样传"整表单 schema"(错误会自动传播到对应字段),也可以在字段级传入 schema 或函数:
validators={{ onChange: ({ value }) => value < 13 ? 'You must be 13 to make an account' : undefined, }}校验时机由你决定——onChange(每次输入)、onBlur(失焦)、onSubmit(提交)甚至挂载时均可。同步与异步校验可以共存(如onBlur+onBlurAsync),异步校验还内置了防抖(asyncDebounceMs与onChangeAsyncDebounceMs等逐项覆盖)。更完整的校验方案请参阅 表单与字段校验指南。
关于onSubmit与提交处理
两种写法都通过onSubmit接收表单值:
onSubmit: ({ value }) => { alert(JSON.stringify(value, null, 2)) },onSubmit中的value是当前表单全部值(深拷贝后的最新状态),类型由defaultValues自动推断。在onSubmit里做异步提交时,建议声明为async函数并返回,库会自动维护isSubmitting状态。提交相关的进阶处理(onSubmitAsync、服务端校验、防止无效表单提交等)可参考 提交处理指南。
设计哲学与使用建议
快速上手页强调"先理解哲学再上手使用"。TanStack Form 的核心哲学(详见 docs/philosophy.md)包括:
- 统一的 API:不为了满足不同口味而碎片化 API,宁可学习曲线稍高,也要降低心智负担;
- 表单需要灵活性:校验时机、作用范围(字段级/表单级/子集)、自定义校验逻辑、自定义错误消息、异步校验均开放;
- 受控即优雅(Controlled is Cool):状态可预测、易于测试、支持非 DOM 渲染器(如 React Native),便于做条件渲染与调试;
- 泛型是痛苦的(Generics are grim):你永远不需要手动传泛型,库从运行时默认值推断一切类型:
// 不要这样: useForm<MyForm>() // 而应这样: interface Person { name: string age: number } const defaultPerson: Person = { name: 'Bill Luo', age: 24 } useForm({ defaultValues: defaultPerson, })- 库是用来被包装的(Libraries are liberating):TanStack Form 的设计目标之一就是被封装进你自己的组件体系或设计系统,
createFormHook正是为此而生——它导出的useAppForm与withForm可以带着预绑定组件在整个应用中保持一致。
安装与下一步
在 React 项目中安装:
npm install @tanstack/react-form如果你想亲眼看到上述两种写法的完整可运行示例,仓库中提供了对应的示例工程:examples/react/simple(useForm一次性写法)、examples/react/composition(createFormHook组合写法),以及大型表单 examples/react/large-form 等多步向导示例,可直接安装依赖后本地运行。
掌握快速上手后,建议按顺序阅读 基础概念、表单组合 与 校验指南,并在 useForm 参考 与 createFormHook 参考 中查阅完整 API 签名。
【免费下载链接】form🤖 Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, and Lit.项目地址: https://gitcode.com/GitHub_Trending/form/form
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考