---
id: typed-issue-form
track: learn
product: form
locale: zh-CN
order: 19
revision: 1
sourceCheckedOn: "2026-08-03"
versionRange: "Form ^1"
verifiedAgainst: "@tanstack/react-form@1.33.3"
contentModel: 2
prerequisites:
  - "理解受控 input 与表单提交事件"
  - "已有经过服务端验证的创建接口"
sourceRefs:
  - "https://tanstack.com/form/latest/docs/framework/react/quick-start"
  - "https://tanstack.com/form/latest/docs/framework/react/guides/validation"
  - "https://tanstack.com/form/latest/docs/framework/react/guides/submission-handling"
---

# 让字段、校验与提交共享同一类型

> 用 useForm、Field 与 Subscribe 建立受控表单，把校验时机、错误显示和异步提交状态写成显式契约。

**Outcome:** 能实现字段名可推导、错误时机明确且不会重复提交的 Issue 表单。

## 默认值决定表单的输入类型

useForm 从 defaultValues 推导字段路径和 onSubmit value。每个 Field 通过 field.state.value 读取值，并用 field.handleChange、field.handleBlur 推进状态。不要混用 React 本地状态、DOM defaultValue 和 Form 的受控值，否则会产生多个事实来源。

## 把校验时机与错误展示分开

onBlur 适合避免用户每输入一个字符就看到错误；onChange 适合即时约束。验证器返回 truthy 错误值表示失败，返回 undefined 表示通过。错误数组可以存在于 meta，但界面通常还应检查 isTouched，避免初次渲染就责备用户。

File: `src/features/issues/IssueForm.tsx`

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

export function IssueForm() {
  const form = useForm({
    defaultValues: { title: '', priority: 'medium' as 'low' | 'medium' | 'high' },
    onSubmit: async ({ value }) => { await createIssue(value) },
  })
  return <form onSubmit={(event) => { event.preventDefault(); form.handleSubmit() }}>
    <form.Field name="title" validators={{
      onBlur: ({ value }) => value.trim() ? undefined : 'Title is required',
    }}>{(field) => <label>Title<input name={field.name} value={field.state.value}
      onBlur={field.handleBlur} onChange={(event) => field.handleChange(event.target.value)} />
      {field.state.meta.isTouched && !field.state.meta.isValid
        ? <span role="alert">{field.state.meta.errors.join(', ')}</span> : null}
    </label>}</form.Field>
    <form.Subscribe selector={(state) => [state.canSubmit, state.isSubmitting]}>
      {([canSubmit, isSubmitting]) => <button type="submit"
        aria-disabled={!canSubmit || isSubmitting} disabled={isSubmitting}>
        {isSubmitting ? 'Saving…' : 'Create issue'}
      </button>}
    </form.Subscribe>
  </form>
}
```

## 提交状态不是服务端信任边界

原生 submit 事件必须 preventDefault 后调用 form.handleSubmit，验证通过才进入 onSubmit。isSubmitting 覆盖异步 onSubmit，可用于阻止重复动作并显示进度。但客户端类型和校验都可绕过；Server Function 或 Server Route 必须重新解析、鉴权并返回可区分的字段错误与系统错误。

## 实践与验证

### 完成检查点

字段名、默认值、校验器和 onSubmit value 共享同一类型，错误只在明确的交互时机显示。

### 动手练习

建立包含 title 与 priority 的 Issue 表单：title 在 blur 后校验非空，提交期间显示状态且防止重复提交。

### 验证方法

测试空标题、有效提交、异步失败和快速双击；无效值不进入 onSubmit，pending 期间只有一个请求，服务端仍独立验证输入。

### 常见错误

- 给 input 同时传 defaultValue 和受控 value。
- 只信任浏览器校验结果，把原始 value 直接写入服务端。

## Official sources

- https://tanstack.com/form/latest/docs/framework/react/quick-start
- https://tanstack.com/form/latest/docs/framework/react/guides/validation
- https://tanstack.com/form/latest/docs/framework/react/guides/submission-handling
