← 编程返回顶部

React Hook Form:React 表单状态管理

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 时
allBlur 和 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

特性watchuseWatch
主要功能获取并订阅表单值获取并订阅表单值
使用方式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

对比ControlleruseController
形式React ComponentReact 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 项目,最重要的是理解:

  1. register 与 Controller 的区别。
  2. defaultValues、isDirty、dirtyFields 的关系。
  3. watch、useWatch、getValues 的差别。
  4. reset 如何初始化和重新定义表单基准。
  5. 服务端验证错误如何通过 setError 显示。
  6. 如何使用 mutateAsync 管理异步提交。
  7. Mutation 成功后如何同步服务器缓存与表单状态。

参考文档