关于 web-forms-tanstack-form
这份指南介绍如何使用 TanStack Form 构建表单,适用于 React 及其他受支持框架的开发者。内容涵盖类型推断、按事件验证、字段数组、共享组件和状态订阅。
主要功能
- 01从 `defaultValues` 推断字段与提交类型
- 02通过 `form.Field` 渲染字段
- 03按事件配置同步与异步验证
- 04处理关联字段和动态数组
- 05用 `createFormHook` 复用表单组件
- 06通过状态选择器减少重新渲染
适用场景
- 01构建带验证的类型化表单
- 02相关值变化时重新验证字段
- 03增删数组形式的字段组
- 04在应用中共享表单组件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-forms-tanstack-form安装后在 Claude Code 中输入/web-forms-tanstack-form确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/arrays.mdcomposition.mdcore.mdvalidation.md详细介绍
TanStack Form 模式
快速指南:
useForm接收defaultValues,并从该对象推断每个字段的名称、值类型和提交 载荷。字段通过form.Field渲染,并使用children渲染属性,该属性会提供field.state.value、field.handleChange和field.handleBlur。验证逻辑 位于validators属性中——按事件(onChange、onBlur、onSubmit)指定,并且每种事件都有对应的Async变体,可用于字段或表单。mode="array"可启用pushValue/removeValue,onChangeListenTo会在其他字段发生变化时重新运行验证器,而form.Subscribe可缩小触发重新渲染的状态变化范围。
详细资源:
- examples/core.md — 完整的表单示例:字段、类型、提交和重置
- examples/validation.md — 同步、异步和跨字段验证器;验证器中的 schema 对象
- examples/arrays.md — 使用
mode="array"创建动态字段组 - examples/composition.md —
createFormHook、useAppForm、监听器 - reference.md — 验证器事件、字段和表单状态表、API 方法、框架包
适用路径
- 单个表单 — 使用
useForm和form.Field渲染属性,无需进行其他设置。请参阅 examples/core.md。 - 整个应用中的表单需要保持一致的行为 —
createFormHook一次性注册共享的字段和 表单组件,useAppForm则在每个调用点替代useForm。请参阅 examples/composition.md。 - React 以外的框架 — 表单核心是共享的,只有包和字段绑定有所不同;reference.md 中的「框架包」表列出了每种框架对应的两者。
<critical_requirements>
编写 TanStack Form 代码前
为 useForm 中的每个字段提供一个 defaultValues 条目。 字段名称、值类型和提交载荷都从该对象推断,因此,未包含在其中的字段对类型来说是未知的。
通过 form.Field 及其 children 渲染属性渲染每个字段。 渲染属性会显式接收值和处理器——此库没有字段注册辅助函数,也不转发 ref,因此,无论以何种其他方式连接输入框,它都不会加入表单。
将验证放在 validators 属性中,并按应触发验证的事件作为键。 onChange、onBlur 和 onSubmit 各自都有对应的 Async 版本,并且字段和表单上都存在相同的属性。
将 field.state.meta.errors 作为数组读取。 它包含该字段当前的所有错误,因此可以对它使用 .map(),或检查 .length;将它与字符串比较时,结果始终不相等。
在表单的 onSubmit 中调用 e.preventDefault(),然后再调用 form.handleSubmit()。 此库不会拦截原生提交,因此不调用它的话,浏览器会在提交过程中跳转离开。
</critical_requirements>
自动检测: @tanstack/react-form、@tanstack/vue-form、@tanstack/solid-form、 @tanstack/angular-form、@tanstack/lit-form、@tanstack/form-core、form.Field、form.Subscribe、 createFormHook、createFormHookContexts、useAppForm、withForm、fieldContext、formContext、 field.handleChange、field.handleBlur、field.state.meta、pushValue、removeValue、swapValues、 onChangeListenTo、onBlurListenTo、setErrorMap、formDevtoolsPlugin
适用范围:
- 表单状态、验证时机和提交
- 跨字段规则,即某个字段的有效性取决于另一个字段的值
- 可添加、移除和重新排序字段组的动态列表
- 通过工厂在应用中共享字段和表单组件
- Vue、Solid、Angular 或 Lit 中的表单,以及 React 中的表单
由其他部分处理:
- 编写验证模式——验证器接受任何 Standard Schema 对象,而该模式如何声明规则,则由其所属方决定。
- 渲染和设置输入框样式——此库不包含 UI;渲染属性会传递值和处理器,标记由你负责。
- 初始值的来源——
defaultValues是一个普通对象,表单不会获取任何内容。
表单采用无头设计,其类型依靠推断。defaultValues 是权威模式:字段名称会根据它提供自动补全,field.state.value 的类型由它确定,onSubmit 载荷也与它匹配——无需泛型参数,也无需担心另一份类型声明与之偏离。
验证绑定到事件,而不是某种模式。每个验证器都会在其所属层级声明运行时机,因此,同一字段上可以将开销低的格式检查放在 onChange,而将开销高的唯一性检查留到 onBlurAsync。
状态通过订阅读取。form.Subscribe 和 useStore 接受一个选择器,并且仅在选择器返回的内容发生变化时重新渲染,因此在组件主体中直接读取 form.state,就会偏离这一整套设计。
核心模式
模式 1:useForm 和 form.Field
渲染属性就是完整的字段 API——传入值,传出处理器,没有任何隐式操作。
const form = useForm({
defaultValues: { name: "", email: "" },
onSubmit: async ({ value }) => {
await submitToApi(value);
},
});
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.Field
name="email"
children={(field) => (
<input
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
</form>;
onBlur={field.handleBlur} 用于将字段标记为已触碰——省略它会导致 isTouched 一直为 false,任何 onBlur 验证器也都不会运行。
完整代码:examples/core.md
模式 2:字段级验证器
同步验证器会返回一条消息字符串;如果值通过验证,则返回 undefined。
<form.Field
name="age"
validators={{
onChange: ({ value }) => (value < 13 ? "Must be 13 or older" : undefined),
onBlurAsync: async ({ value }) => {
const ok = await checkAge(value);
return ok ? undefined : "Age not valid on server";
},
}}
children={(field) => (/* ... */)}
/>
同步验证会先于异步验证:如果同时提供了 onBlur 和 onBlurAsync,则只有同步验证通过后才会运行异步验证——因此,对于已知无效的值,不会触发网络请求。
模式 3:关联字段
onChangeListenTo 指定当这些字段发生变化时,应重新运行此字段的验证器。
<form.Field
name="confirm_password"
validators={{
onChangeListenTo: ["password"],
onChange: ({ value, fieldApi }) =>
value !== fieldApi.form.getFieldValue("password")
? "Passwords do not match"
: undefined,
}}
children={(field) => (/* ... */)}
/>
如果没有它,编辑 password 后,confirm_password 上的错误仍会显示旧比较结果,直到用户再次操作确认字段。
模式 4:数组字段
mode="array" 会为字段提供 pushValue、removeValue、insertValue、swapValues 和
moveValue。嵌套字段通过索引定位数组项。
<form.Field
name="hobbies"
mode="array"
children={(hobbies) => (
<div>
{hobbies.state.value.map((_, i) => (
<form.Field
key={i}
name={`hobbies[${i}].name`}
children={(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
))}
<button type="button" onClick={() => hobbies.pushValue({ name: "" })}>
Add hobby
</button>
</div>
)}
/>
完整代码:examples/arrays.md
模式 5:表单级验证器
useForm 上的验证器可以一次查看所有值,因此服务端验证适合放在这里,因为它可以将错误归因到各个字段。
const form = useForm({
defaultValues: { username: "", age: 0 },
validators: {
onSubmitAsync: async ({ value }) => {
const errors = await validateOnServer(value);
if (!errors) return null;
return {
form: "Submission failed",
fields: { username: errors.username, age: errors.age },
};
},
},
});
返回结构为 { form?: string, fields: Record<string, string> },null 表示有效。
这与字段验证器不同,字段验证器返回的是一个裸字符串。
模式 6:createFormHook
此工厂只需注册一次字段和表单组件,这样每个表单就可以通过 form.AppField 和 form.AppForm 使用它们,而不必重复编写 render-prop 标记。
export const { fieldContext, formContext, useFieldContext } =
createFormHookContexts();
export const { useAppForm, withForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: { TextField, SelectField },
formComponents: { SubmitButton },
});
useAppForm 接受 useForm 所接受的所有参数。
模式 7:监听器
监听器响应字段事件并触发副作用。它们不返回任何内容——只有验证器会返回错误。
<form.Field
name="country"
listeners={{
onChange: () => form.setFieldValue("province", ""),
}}
children={(field) => (/* ... */)}
/>
可用事件:onChange、onBlur、onMount、onSubmit。
模式 8:form.Subscribe
selector 决定哪些变化会触发重新渲染。将其范围缩小到实际渲染的状态。
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting] as const}
children={([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit || isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit"}
</button>
)}
/>
完整代码:examples/core.md
<red_flags>
易踩问题
运行时会出错:
- 调用
form.handleSubmit()时没有e.preventDefault()——浏览器会原生提交表单,页面会在提交过程中重新加载。 defaultValues缺少某个字段——其field.state.value为undefined,输入框会以非受控方式挂载,并且字段类型未知。- 将
field.state.meta.errors与字符串比较——它是一个数组,因此比较结果总是 false,消息也永远不会渲染。应对它调用.map()。 - 将不完整的对象传给
pushValue——它不符合数组元素类型,缺少的键会导致相应输入框处于非受控状态。 - 在
onSubmit中抛出错误——form.handleSubmit()不会捕获该错误。应在回调中捕获,并通过form.setErrorMap()显示错误。 - 使用点号表示法访问数组项——字段路径是
items[0].name,而items.0.name不会指向任何内容。
容易令人意外的行为:
- 在组件主体中读取
form.state会订阅每次状态变化。使用带选择器的form.Subscribe,或useStore(form.store, selector),可以缩小订阅范围。 form.Subscribe不带selector时会订阅所有内容,开销相同。- 同步验证器验证失败会阻止其异步对应项运行——这是有意设计的,这意味着单独使用异步验证器时没有低开销的预检查。
- 表单级验证器返回
{ form?, fields },而字段验证器返回字符串——在表单级验证器中返回的字段结构会被静默忽略。 - 通过
createFormHook注册的组件位于form.AppField和form.AppForm上;form.Field仍然存在,且仍接受普通的 render prop。
reference.md 中提供了其中最常见问题的代码修改前后示例。
</red_flags>