mcpField
W
web-forms-tanstack-form

web-forms-tanstack-form

github.com/agents-inc开发2663 次安装2 天前更新
代码仓库

关于 web-forms-tanstack-form

这份指南介绍如何使用 TanStack Form 构建表单,适用于 React 及其他受支持框架的开发者。内容涵盖类型推断、按事件验证、字段数组、共享组件和状态订阅。

主要功能

  1. 01从 `defaultValues` 推断字段与提交类型
  2. 02通过 `form.Field` 渲染字段
  3. 03按事件配置同步与异步验证
  4. 04处理关联字段和动态数组
  5. 05用 `createFormHook` 复用表单组件
  6. 06通过状态选择器减少重新渲染

适用场景

  1. 01构建带验证的类型化表单
  2. 02相关值变化时重新验证字段
  3. 03增删数组形式的字段组
  4. 04在应用中共享表单组件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/agents-inc/skills --skill web-forms-tanstack-form

安装后在 Claude Code 中输入/web-forms-tanstack-form确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.yaml
reference.md
examples/
arrays.md
composition.md
core.md
validation.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 可缩小触发重新渲染的状态变化范围。

详细资源:


适用路径

  • 单个表单 — 使用 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,则只有同步验证通过后才会运行异步验证——因此,对于已知无效的值,不会触发网络请求。

完整代码:examples/validation.md


模式 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 上的错误仍会显示旧比较结果,直到用户再次操作确认字段。

完整代码:examples/validation.md


模式 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 表示有效。 这与字段验证器不同,字段验证器返回的是一个裸字符串。

完整代码:examples/validation.md


模式 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 所接受的所有参数。

完整代码:examples/composition.md


模式 7:监听器

监听器响应字段事件并触发副作用。它们不返回任何内容——只有验证器会返回错误。

<form.Field
  name="country"
  listeners={{
    onChange: () => form.setFieldValue("province", ""),
  }}
  children={(field) => (/* ... */)}
/>

可用事件:onChange、onBlur、onMount、onSubmit。

完整代码:examples/composition.md


模式 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>

评论

支持 Markdown