能实现字段名可推导、错误时机明确且不会重复提交的 Issue 表单。
默认值决定表单的输入类型
useForm 从 defaultValues 推导字段路径和 onSubmit value。每个 Field 通过 field.state.value 读取值,并用 field.handleChange、field.handleBlur 推进状态。不要混用 React 本地状态、DOM defaultValue 和 Form 的受控值,否则会产生多个事实来源。
把校验时机与错误展示分开
onBlur 适合避免用户每输入一个字符就看到错误;onChange 适合即时约束。验证器返回 truthy 错误值表示失败,返回 undefined 表示通过。错误数组可以存在于 meta,但界面通常还应检查 isTouched,避免初次渲染就责备用户。
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 直接写入服务端。