React Hook Form(RHF)是 React 生态中常用的表单管理库。
主要负责:
- 注册和管理表单字段。
- 收集用户输入。
- 字段验证和错误管理。
- 表单提交。
- 管理 Dirty、Touched、Submitting 等状态。
- 尽可能减少不必要的 React 重新渲染。
React Hook Form 主要管理客户端表单状态,而 TanStack Query 主要管理服务器状态。
典型工作流程:
Django API
↓
TanStack Query
↓
Server Data
↓
React Hook Form
│
├── Field Registration
├── Form Values
├── Validation
├── Error Management
└── Form State
↓
handleSubmit()
↓
TanStack Query Mutation
↓
Axios
↓
Django API
1. 基础使用
安装:
pnpm add react-hook-form
基本示例:
import { useForm } from "react-hook-form";
type FormData = {
username: string;
email: string;
};
function ProfileForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({
defaultValues: {
username: "",
email: "",
},
});
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register("username", {
required: "Username is required",
})}
/>
{errors.username && <p>{errors.username.message}</p>}
<input
{...register("email", {
required: "Email is required",
})}
/>
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</form>
);
}
与传统 React 表单相比,RHF 不需要为每个输入字段单独创建 useState。
2. useForm:表单控制器
useForm() 是 RHF 的核心 Hook。
const form = useForm<FormData>({
defaultValues: {
username: "",
email: "",
},
mode: "onSubmit",
});
它创建并返回一组表单管理方法:
const {
register,
control,
handleSubmit,
watch,
getValues,
setValue,
reset,
trigger,
setError,
clearErrors,
formState,
} = useForm<FormData>();
useForm 常用配置
| 参数 | 作用 |
|---|---|
defaultValues | 初始化表单值 |
values | 根据外部数据响应式更新表单值 |
mode | 首次验证的触发模式 |
reValidateMode | 提交后错误字段的重新验证模式 |
resolver | 接入外部验证库 |
criteriaMode | 控制每个字段收集多少验证错误 |
shouldFocusError | 验证失败时是否聚焦错误字段 |
shouldUnregister | 字段卸载时是否取消注册 |
disabled | 禁用整个表单 |
Validation Mode
| Mode | 验证时机 |
|---|---|
onSubmit | 提交时,默认 |
onBlur | 字段失去焦点时 |
onChange | 输入变化时 |
onTouched | 首次 Blur 后,后续 Change 时 |
all | Blur 和 Change 时 |
例如:
useForm<FormData>({
mode: "onBlur",
reValidateMode: "onChange",
});
表示首次验证主要在 Blur 时触发,提交后已有错误字段在 Change 时重新验证。
onChange 模式可能频繁执行验证,因此对于复杂表单需要注意性能。
3. register:注册表单字段
register() 将原生输入元素注册到 RHF。
<input {...register("username")} />
大致相当于:
<input
name="username"
onChange={...}
onBlur={...}
ref={...}
/>
register() 主要返回:
name:字段名称。onChange:字段变化处理函数。onBlur:失去焦点处理函数。ref:输入元素引用。
与传统受控组件的区别
传统 React:
const [username, setUsername] = useState("");
<input
value={username}
onChange={(event) => setUsername(event.target.value)}
/>;
RHF:
<input {...register("username")} />
RHF 对原生输入通常采用偏非受控的管理方式,不需要每次输入都通过组件自身的 setState 来更新字段值。
这有利于减少不必要的组件重新渲染。
嵌套对象
register("address.city");
register("address.country");
对应:
{
address: {
city: "Tokyo",
country: "Japan"
}
}
数组字段
register("users.0.name");
对应:
{
users: [{ name: "Alice" }];
}
转换输入类型
HTML Input 的值通常是字符串。
可以使用:
<input
type="number"
{...register("age", {
valueAsNumber: true,
})}
/>
或者:
<input
{...register("price", {
setValueAs: (value) => Number(value),
})}
/>
避免在表单提交后再手动转换所有数值字段。
4. handleSubmit:表单提交
handleSubmit() 负责提交前验证,并调用相应回调。
Browser Submit
↓
handleSubmit
↓
Validation
│
├── Success → onValid(data)
│
└── Failure → onInvalid(errors)
基础用法
const onSubmit = (data: FormData) => {
console.log(data);
};
<form onSubmit={handleSubmit(onSubmit)}>
handleSubmit(onSubmit) 返回可以作为表单提交处理函数使用的函数。
成功与失败回调
import type {
SubmitHandler,
SubmitErrorHandler,
} from "react-hook-form";
const onValid: SubmitHandler<FormData> = (data) => {
console.log(data);
};
const onInvalid: SubmitErrorHandler<FormData> = (errors) => {
console.log(errors);
};
<form onSubmit={handleSubmit(onValid, onInvalid)}>
异步提交
const onSubmit = async (data: FormData) => {
try {
await updateProfile(data);
} catch (error) {
console.error(error);
}
};
对于异步提交,handleSubmit 会等待回调返回的 Promise。
RHF 不会自动吞掉提交处理函数抛出的异常,因此应当自行处理。
5. Validation:字段验证
最简单的验证可以直接写在 register() 中。
<input
{...register("username", {
required: "Username is required",
minLength: {
value: 3,
message: "At least 3 characters",
},
maxLength: {
value: 20,
message: "At most 20 characters",
},
})}
/>
常用验证规则
| 规则 | 作用 |
|---|---|
required | 必填 |
minLength | 最小长度 |
maxLength | 最大长度 |
min | 最小数值 |
max | 最大数值 |
pattern | 正则表达式 |
validate | 自定义验证函数 |
例如:
register("age", {
valueAsNumber: true,
validate: (value) => value >= 18 || "Must be at least 18",
});
自定义异步验证
register("username", {
validate: async (value) => {
const available = await checkUsername(value);
return available || "Username already exists";
},
});
这类验证可能需要防抖或调整验证触发时机,避免每次输入都调用服务器。
同时,前端验证不能代替 Django Serializer 的服务端验证。
6. Resolver:外部验证库
Resolver 是 RHF 与外部 Schema Validation Library 之间的适配器。
常见验证库包括:
- Zod
- Yup
- Valibot
- Joi
安装 Zod:
pnpm add zod @hookform/resolvers
示例:
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
const schema = z.object({
username: z.string().min(3).max(20),
email: z.email(),
});
type FormData = z.infer<typeof schema>;
const form = useForm<FormData>({
resolver: zodResolver(schema),
});
resolver 负责把外部验证结果转换为 RHF 能理解的字段错误。
是否必须使用 Zod?
不必须。
简单表单可以直接使用:
register("username", {
required: true,
minLength: 3,
});
如果需要跨字段验证、复杂对象验证或复用 Schema,可以考虑 Zod。
如果项目已经通过 OpenAPI + Orval 生成了 TypeScript 类型,则可以根据实际需求选择是否增加客户端运行时验证,不必机械地重复定义所有 API Schema。
7. formState:表单状态
formState 是 RHF 中非常重要的一组状态。
const {
formState: {
errors,
isDirty,
dirtyFields,
touchedFields,
isValid,
isValidating,
isSubmitting,
isSubmitted,
isSubmitSuccessful,
submitCount,
},
} = useForm<FormData>();
| 状态 | 含义 |
|---|---|
errors | 字段验证错误 |
isDirty | 当前整体表单值是否与默认值不同 |
dirtyFields | 各字段的 Dirty 状态 |
touchedFields | 用户触碰过的字段 |
isValid | 表单验证结果是否有效 |
isValidating | 是否正在进行验证 |
isSubmitting | 是否正在执行提交回调 |
isSubmitted | 是否已经提交过 |
isSubmitSuccessful | 最近一次提交是否成功完成 |
submitCount | 提交次数 |
isDirty 与 dirtyFields
假设:
defaultValues: {
username: "Alice",
email: "alice@example.com",
}
用户将用户名改成 Bob:
isDirty = true
dirtyFields = {
username: true
}
如果用户再把用户名改回 Alice:
isDirty = false
因为整个表单的当前值重新与 defaultValues 一致。
但 dirtyFields 有字段级状态语义,不应认为它与 isDirty 始终完全同步。
建议为全部字段提供完整、稳定的 defaultValues,否则 Dirty 判断可能不符合预期。
touchedFields
字段获得焦点后再 Blur:
Focus → Blur
↓
touchedFields.username = true
不要求字段值发生修改。
isSubmitting
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit"}
</button>
注意:isSubmitting 跟踪的是 handleSubmit 的提交过程。
如果 onSubmit 只是调用:
mutation.mutate(data);
然后立即返回,isSubmitting 通常不会持续等待 Mutation 完成。
应该使用:
const onSubmit = async (data: FormData) => {
await mutation.mutateAsync(data);
};
或者直接使用 TanStack Query 的 mutation.isPending 管理服务器提交状态。
8. watch:订阅表单值
watch() 用于获取表单值,并让 UI 随字段变化更新。
例如:
const country = watch("country");
return (
<>
<select {...register("country")}>
<option value="JP">Japan</option>
<option value="US">United States</option>
</select>
{country === "JP" && <input {...register("prefecture")} />}
</>
);
当 country 发生变化时,组件可以根据新的值显示不同字段。
常见调用方式
watch(); // 监听整个表单
watch("username"); // 监听单字段
watch(["city", "country"]); // 监听多个字段
watch() 可能使使用它的组件在相关值变化时重新渲染。
对于复杂表单,建议避免在最顶层无差别监听所有字段。
9. useWatch:局部订阅
useWatch() 与 watch() 类似,但可以将重新渲染的影响限制在使用它的组件范围内。
import { useWatch } from "react-hook-form";
function CountryDetails({ control }: { control: Control<FormData> }) {
const country = useWatch({
control,
name: "country",
});
return <p>Selected country: {country}</p>;
}
watch 与 useWatch
| 特性 | watch | useWatch |
|---|---|---|
| 主要功能 | 获取并订阅表单值 | 获取并订阅表单值 |
| 使用方式 | useForm 返回的方法 | 独立 Hook |
| 更新范围 | 可能影响表单根组件 | 更容易隔离在子组件 |
| 适用场景 | 简单表单 | 复杂表单、局部订阅 |
例如,一个包含几十个字段的表单,如果只有某个子组件需要监听 country,通常更推荐在该子组件内使用 useWatch()。
10. getValues:读取当前值
getValues() 可以直接读取当前字段值,但不建立订阅。
const username = getValues("username");
常用于事件处理函数:
const handleSomething = () => {
const username = getValues("username");
console.log(username);
};
watch 与 getValues
| 方法 | 是否订阅 | 用途 |
|---|---|---|
| watch | 是 | 值变化时更新 UI |
| useWatch | 是 | 局部组件更新 |
| getValues | 否 | 临时读取当前值 |
如果只是按钮点击时读取一次值,通常使用 getValues() 即可。
11. setValue:主动修改字段
setValue() 用于通过代码修改表单值。
setValue("username", "Bob");
也可以主动触发状态更新:
setValue("username", "Bob", {
shouldDirty: true,
shouldValidate: true,
shouldTouch: true,
});
| 参数 | 作用 |
|---|---|
shouldDirty | 更新 Dirty 状态 |
shouldValidate | 执行验证 |
shouldTouch | 更新 Touched 状态 |
示例
用户选择某个城市后,自动填写国家:
setValue("country", "Japan", {
shouldDirty: true,
});
注意:setValue() 不等同于 React 的 setState()。
RHF 会根据订阅、错误及状态变化决定需要更新哪些组件,通常不需要每次都导致整个表单重新渲染。
12. reset:重置表单
reset() 用于重置表单的值及相关状态。
reset();
通常恢复到当前 defaultValues。
也可以:
reset({
username: "Alice",
email: "alice@example.com",
});
在默认配置下,提供的新值会成为新的默认值基准。
因此:
Current Values = New Values
Default Values = New Values
isDirty = false
异步获取用户资料
const { data: user } = useGetUser();
const form = useForm<FormData>({
defaultValues: {
username: "",
email: "",
},
});
useEffect(() => {
if (user) {
form.reset({
username: user.username,
email: user.email,
});
}
}, [user, form.reset]);
保护用户正在编辑的数据
如果服务器重新获取了资料,但用户已经修改部分字段,可以考虑:
reset(newData, {
keepDirtyValues: true,
});
这样 Dirty 字段保留用户当前输入,其他字段根据新数据更新。
这对于结合 TanStack Query 的后台 Refetch 尤其重要。
否则,如果每次 Query 数据更新都无条件执行 reset(data),可能覆盖用户尚未保存的输入。
13. values:响应式外部数据
较新版本 RHF 支持使用 values 将外部数据同步到表单中。
例如:
const form = useForm<FormData>({
defaultValues: {
username: "",
email: "",
},
values: user
? {
username: user.username,
email: user.email,
}
: undefined,
});
与 defaultValues 不同,values 会响应外部数据变化。
需要注意:外部数据更新也可能影响用户当前编辑的值。
因此,编辑服务器已有数据时,可以根据需求选择:
- 只初始化一次:使用
reset()或合适的values方案。 - 服务器数据持续同步:使用
values。 - 保留用户已修改字段:结合
keepDirtyValues。
14. Controller:受控组件适配器
RHF 对原生输入可以直接使用 register()。
但是一些第三方 UI 组件使用受控模式,例如:
- Radix UI Select。
- React Select。
- 日期选择器。
- 自定义 Switch。
- 自定义组合输入组件。
这时通常使用 Controller。
import { Controller } from "react-hook-form";
<Controller
name="city"
control={control}
render={({ field, fieldState }) => (
<>
<Select
value={field.value}
onValueChange={field.onChange}
onBlur={field.onBlur}
/>
{fieldState.error && <p>{fieldState.error.message}</p>}
</>
)}
/>;
Controller 的作用
Controller 将受控组件的值变化与 RHF 联系起来。
通常提供:
field.name;
field.value;
field.onChange;
field.onBlur;
field.ref;
其中:
value:当前值。onChange:将新值写回 RHF。onBlur:通知 RHF 字段失去焦点。ref:关联底层输入元素,支持聚焦等功能。
对于非原生组件,是否能直接传入 ref 取决于组件是否暴露对应的输入元素引用。
15. useController
useController() 提供与 Controller 类似的能力,但采用 Hook 形式。
const { field, fieldState, formState } = useController({
name: "username",
control,
});
使用:
<Input
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
适合封装通用表单组件:
FormInput
FormSelect
FormSwitch
FormDatePicker
Controller 与 useController
| 对比 | Controller | useController |
|---|---|---|
| 形式 | React Component | React Hook |
| 主要用途 | 直接适配第三方组件 | 封装可复用表单组件 |
| 提供 field | 是 | 是 |
| 提供 fieldState | 是 | 是 |
两者底层使用相近的控制机制,通常选择其中一种即可,不必同时为同一字段重复注册。
16. useFieldArray:动态字段数组
useFieldArray() 用于管理可以动态添加、删除或排序的字段数组。
例如旅行计划:
{
destinations: [{ city: "Tokyo" }, { city: "Kyoto" }];
}
示例
type FormData = {
destinations: {
city: string;
}[];
};
const { register, control } = useForm<FormData>({
defaultValues: {
destinations: [{ city: "" }],
},
});
const { fields, append, remove, update, move } = useFieldArray({
control,
name: "destinations",
});
渲染:
{
fields.map((field, index) => (
<input key={field.id} {...register(`destinations.${index}.city`)} />
));
}
新增:
append({ city: "" });
删除:
remove(1);
移动:
move(0, 2);
常用方法
| 方法 | 作用 |
|---|---|
append | 末尾添加 |
prepend | 开头添加 |
insert | 指定位置插入 |
remove | 删除 |
swap | 交换两个元素 |
move | 移动元素 |
update | 替换指定项 |
replace | 替换整个数组 |
注意事项
必须使用:
key={field.id}
而不是简单使用:
key = { index };
因为数组插入或删除后,索引可能变化。
另外,update() 可能导致相应字段重新挂载。如果只需要修改某个字段的值,可以考虑使用 setValue()。
17. FormProvider 与 useFormContext
复杂表单通常会拆分为多个子组件:
ProfileForm
├── BasicInfoSection
│ ├── UsernameField
│ └── EmailField
├── AddressSection
│ ├── CountryField
│ └── CityField
└── SubmitButton
如果逐层传递:
register;
control;
errors;
setValue;
会产生大量 Props。
FormProvider 可以通过 React Context 共享 RHF 的表单控制器。
父组件
import { FormProvider, useForm } from "react-hook-form";
function ProfileForm() {
const methods = useForm<FormData>();
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<BasicInfoSection />
<AddressSection />
<SubmitButton />
</form>
</FormProvider>
);
}
子组件
function UsernameField() {
const {
register,
formState: { errors },
} = useFormContext<FormData>();
return (
<>
<input {...register("username")} />
{errors.username && <p>{errors.username.message}</p>}
</>
);
}
核心作用
useForm()
↓
FormProvider
↓
React Context
↓
useFormContext()
↓
Child Components
不需要再将 register、control 等方法逐层传递。
注意:通常不应在同一个表单中无必要地嵌套多个 FormProvider。
18. useFormState:局部表单状态订阅
useFormState() 可以让组件只订阅需要的表单状态。
例如:
import { useFormState } from "react-hook-form";
function SubmitButton() {
const { control } = useFormContext<FormData>();
const { isDirty, isSubmitting } = useFormState({
control,
});
return (
<button type="submit" disabled={!isDirty || isSubmitting}>
Save
</button>
);
}
这样 SubmitButton 可以独立订阅相关状态。
useFormState 的价值
对于大型表单,避免每个字段更新都导致整个表单根组件重新渲染。
尤其适用于:
- 独立错误信息组件。
- SubmitButton。
- 字段分组。
- 复杂嵌套表单。
RHF 的表单状态使用基于订阅的优化机制,建议只读取组件真正需要的状态。
19. trigger:主动执行验证
trigger() 可以手动执行字段验证。
验证整个表单
const valid = await trigger();
if (valid) {
console.log("All fields are valid");
}
验证单个字段
await trigger("email");
验证多个字段
await trigger(["username", "email"]);
常见用途
- 多步骤表单。
- 进入下一步之前进行验证。
- 字段 A 改变后,重新验证字段 B。
- 在特定按钮操作之前主动检查输入。
例如:
const handleNextStep = async () => {
const valid = await trigger(["username", "email"]);
if (valid) {
setStep(2);
}
};
trigger() 返回布尔值,表示此次验证是否通过。
20. setError:手动设置错误
setError() 用于将错误写入 RHF。
setError("username", {
type: "manual",
message: "Username already exists",
});
随后可以通过:
errors.username?.message;
读取错误信息。
处理服务端验证错误
例如 Django DRF 返回:
{
"username": ["This username already exists."]
}
可以将服务端错误映射到表单字段:
setError("username", {
type: "server",
message: "This username already exists.",
});
非字段级错误
例如服务器返回通用错误:
setError("root.server", {
type: "server",
message: "Failed to save profile",
});
在 UI 中显示:
{
errors.root?.server && <p>{errors.root.server.message}</p>;
}
setError 与 isValid
setError() 可以手动添加错误状态,但 isValid 的最终结果主要由表单验证规则计算。
因此,不应该将 setError() 理解为永久控制 isValid 的开关。
当字段重新通过验证时,手动设置在已注册字段上的错误也可能被清除。
21. clearErrors:清除错误
clearErrors() 用于手动清除表单错误。
清除单个字段
clearErrors("username");
清除多个字段
clearErrors(["username", "email"]);
清除全部错误
clearErrors();
清除服务端错误
clearErrors("root.server");
例如用户重新编辑字段时,可以清除之前的服务器错误:
<input
{...register("username", {
onChange: () => {
clearErrors("root.server");
},
})}
/>
clearErrors 与 trigger 的区别
| 方法 | 作用 |
|---|---|
clearErrors() | 清除当前存储的错误 |
trigger() | 重新运行验证 |
setError() | 手动添加错误 |
重要:clearErrors() 不会修改验证规则,也不会自动触发验证或使 isValid 变为 true。 如果值仍然不合法,下一次验证时错误可能重新出现。
22. React Hook Form + TanStack Query
对于编辑服务器已有数据的表单,常见流程:
TanStack Query
↓
GET User Profile
↓
React Hook Form
↓
User Edits Fields
↓
handleSubmit
↓
useMutation
↓
PATCH User Profile
↓
Update Query Cache
↓
reset(savedData)
示例
type ProfileFormData = {
username: string;
email: string;
};
function ProfileForm({ initialData }: { initialData: ProfileFormData }) {
const queryClient = useQueryClient();
const {
register,
handleSubmit,
reset,
setError,
formState: { errors, isDirty },
} = useForm<ProfileFormData>({
defaultValues: initialData,
});
const mutation = useUserProfilePartialUpdate();
const onSubmit = async (data: ProfileFormData) => {
try {
const updatedProfile = await mutation.mutateAsync({
data,
});
queryClient.setQueryData(
getUserProfileRetrieveQueryKey(),
updatedProfile,
);
reset({
username: updatedProfile.username,
email: updatedProfile.email,
});
} catch (error) {
setError("root.server", {
type: "server",
message: "Unable to save profile",
});
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("username")} />
<input {...register("email")} />
{errors.root?.server && <p>{errors.root.server.message}</p>}
<button type="submit" disabled={!isDirty || mutation.isPending}>
{mutation.isPending ? "Saving..." : "Save"}
</button>
</form>
);
}
上述示例假设 Mutation 返回完整的最新 Profile 数据。
如果后端仅返回 204 或简单成功状态,则应使用 invalidateQueries() 重新获取资料,而不是直接将 Mutation 返回值写入 Profile 缓存。
为什么保存后 reset?
因为保存成功后,当前数据已经成为服务器认可的新值。
例如:
原始 username = Alice
↓
用户修改为 Bob
↓
isDirty = true
↓
服务器保存 Bob
↓
reset({ username: "Bob" })
↓
isDirty = false
这样用户继续编辑时,Dirty 状态会相对于刚刚保存的资料判断。
23. 文件上传
对于文件字段,可以使用:
<input type="file" {...register("avatar")} />
RHF 通常会接收到 FileList。
例如:
type AvatarFormData = {
avatar: FileList;
};
const onSubmit = async (data: AvatarFormData) => {
const file = data.avatar?.[0];
if (!file) return;
const formData = new FormData();
formData.append("avatar", file);
await uploadAvatar(formData);
};
对于 Orval 生成的上传函数,应该以 OpenAPI Schema 以及实际生成的参数类型为准,避免重复构建 FormData。
另外,浏览器文件输入无法像普通文本输入一样任意设置文件值。因此文件字段的重置和默认值处理需要单独考虑。
24. 性能优化
React Hook Form 的优势之一是减少不必要的重新渲染。
常用策略:
1. 原生输入优先使用 register
<input {...register("username")} />
没有必要为每个原生 Input 都使用 Controller。
2. 复杂组件使用 Controller
<Controller
name="city"
control={control}
render={({ field }) => (
<CustomSelect value={field.value} onValueChange={field.onChange} />
)}
/>
3. 避免全表单 watch
不推荐在大型表单顶层无差别使用:
watch();
可以改为在需要的子组件中使用:
useWatch({
control,
name: "country",
});
4. 局部订阅 formState
使用:
useFormState({
control,
});
避免将所有状态都交由顶层组件读取和渲染。
5. 提供完整的 defaultValues
useForm<FormData>({
defaultValues: {
username: "",
email: "",
},
});
这有利于准确判断 Dirty 状态,并避免受控组件遇到 undefined 初始值的问题。
6. 不要频繁 reset
如果每次服务器 Refetch 都调用 reset(),可能覆盖用户正在编辑的数据。
可以使用 keepDirtyValues 或建立只初始化一次的策略。
25. 常见问题
Q1:React Hook Form 能替代 useState 吗?
只能替代表单字段状态管理中的一部分 useState。
Modal 是否打开、当前 Tab、页面 UI 状态等仍然适合使用 React State。
Q2:RHF 和 TanStack Query 是否功能重叠?
有少量交集,但主要职责不同。
- RHF:用户尚未提交的临时表单状态。
- TanStack Query:服务器数据、缓存及请求状态。
两者通常结合使用。
Q3:为什么已经有 defaultValues,还要使用 reset?
因为 defaultValues 主要用于初始化。
服务器数据通常是异步获取的,获取完成后需要同步到表单。reset() 是常用方式之一。
Q4:Controller 是否比 register 更先进?
不是。
二者面向不同组件类型。
原生 Input 通常使用 register 更简单;复杂受控组件使用 Controller 更合适。
Q5:表单验证已经通过,为什么服务器仍然返回 400?
因为前端验证只是用户体验优化,不能保证服务器一定接受数据。
服务器可能存在额外的:
- 数据库唯一约束。
- 权限检查。
- 业务规则。
- 并发状态变化。
- 字段转换和校验。
因此必须保留 Django Serializer 的服务端验证。
总结
React Hook Form 的核心内容可以分为四个层次:
| 分类 | 重点 |
|---|---|
| 基础表单 | useForm、register、handleSubmit、validation |
| 表单状态 | formState、watch、getValues、setValue、reset |
| 复杂组件 | Controller、useController、useFieldArray、FormProvider |
| 高级能力 | resolver、trigger、setError、clearErrors、useWatch、useFormState |
核心流程:
useForm()
↓
register / Controller
↓
Form Values
↓
Validation
↓
handleSubmit()
↓
TanStack Query Mutation
↓
Axios
↓
Django API
↓
Cache Update / Invalidation
↓
reset()
对于 React + TypeScript + Orval + TanStack Query 项目,最重要的是理解:
register与Controller的区别。defaultValues、isDirty、dirtyFields的关系。watch、useWatch、getValues的差别。reset如何初始化和重新定义表单基准。- 服务端验证错误如何通过
setError显示。 - 如何使用
mutateAsync管理异步提交。 - Mutation 成功后如何同步服务器缓存与表单状态。