使用 React Hook Form 现代、高效的处理表单

三葉Leaves Author

核心心法:非受控组件 (Uncontrolled Components)

RHF 管理的表单,在数据更改时不会立刻渲染,而是类似于非受控组件那样特定时刻部分渲染,这带来了更高的性能。

但是同时,它又提供了一系列很实用的工具,能让你享受到受控组件一般的开发管理体验。

要理解 RHF,首先必须理解受控组件非受控组件的区别。

  • 受控组件 (Controlled Component):这是我们常规的 React 思维。表单元素的值由 React 的 state 来完全控制。用户的每一次输入(onChange)都会触发 setState,从而导致组件重新渲染。

    1
    2
    const [value, setValue] = useState('');
    <input value={value} onChange={(e) => setValue(e.target.value)} />

    优点:数据流清晰,状态始终与 React state 同步,便于实时校验和动态控制。
    缺点:频繁的 re-render 会带来性能开销,尤其是在大型复杂表单中。

  • 非受控组件 (Uncontrolled Component):表单数据由 DOM 自身来处理和存储。 React 不直接控制输入框的值,而是在需要时通过 ref 去 DOM 中读取。

    1
    2
    3
    const inputRef = useRef(null);
    <input ref={inputRef} />
    // 在提交时通过 inputRef.current.value 读取值

    优点:性能极高,因为用户的输入不会触发 React 的重新渲染。
    缺点:与 React 的声明式思想略有出入,数据流不是单向的,状态管理相对麻烦。

React Hook Form 的天才之处,就在于它以非受控组件为基础,通过 Hooks 的方式巧妙地封装了状态管理和校验逻辑,让你既能享受到非受控组件的性能优势,又能拥有媲美受控组件的开发体验和强大功能。


第一个上手实例:useForm & register

现在,让我们通过一个最简单的例子,来看看 RHF 的魅力。

假设我们要创建一个包含用户名和邮箱的表单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { useForm } from 'react-hook-form';

function MyForm() {
// 1. 调用 useForm Hook 获取核心方法
const { register, handleSubmit, formState: { errors } } = useForm();

// 2. 创建提交处理函数
const onSubmit = (data) => {
console.log(data); // { username: '张三', email: 'zhangsan@example.com' }
};

return (
// 3. 将 handleSubmit 包裹我们的提交函数
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>用户名</label>
{/* 4. 使用 register "注册" input 到 RHF 中 */}
<input {...register("username", { required: "用户名为必填项" })} />
{/* 5. 显示校验错误信息 */}
{errors.username && <p>{errors.username.message}</p>}
</div>

<div>
<label>邮箱</label>
<input
{...register("email", {
required: "邮箱为必填项",
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "请输入有效的邮箱地址"
}
})}
/>
{errors.email && <p>{errors.email.message}</p>}
</div>

<button type="submit">提交</button>
</form>
);
}

让我们来逐行剖析这段代码,理解其底层原理:

1. useForm()

这是 RHF 的大脑。调用它会返回一个对象,里面包含了管理整个表单所需的所有工具,大体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
register: Function,
handleSubmit: Function,
formState: {
errors: Object,
isDirty: boolean,
isValid: boolean,
isSubmitting: boolean,
... // 还有很多
},
reset: Function,
watch: Function,
setValue: Function,
...
}

我们这里解构了最常用的三个:register, handleSubmit, 和 formState

2. register("username", { ...validationRules })

这是连接 React 与 DOM 的桥梁。

当你使用扩展运算符 {...register("username")} 时,RHF 会默默地为你向 <input> 元素注入几个关键的 props,包括

  • name="username",这是第一个参数指定的,后续会作为提交的 json 的键。
  • ref,这是因为上文说的非受控组件原因。
  • onChange
  • onBlur 等。

关键点:它会通过 ref 获取这个 DOM 节点的引用。当用户输入时,RHF 内部会监听 onChange 事件,但它不会立即触发 React 的 re-render。它只是在内部更新该字段的值。这就是性能优势的来源。

第二个参数是一个配置对象,用于定义校验规则。 RHF 内置了 required, minLength, maxLength, pattern 等许多与原生 HTML 规范一致的规则。实际开发中,更可以结合 Zod 等第三方库作为其插件,请见:

使用 Zod 进行表单校验

3. handleSubmit(onSubmit)

这是一个高阶函数,扮演着“提交守门员”的角色。

  1. 当你点击提交按钮时,handleSubmit 会首先执行。它会阻止表单的默认提交行为

  2. 然后,它会触发内部的校验逻辑,收集所有通过 register 注册的字段的值。这个过程很像是 [[使用 form-serialize.js 一次拿到表单所有值 | form-serialize.js]] 做的事情。

  3. 校验:

    • 如果校验通过,它会把你定义 onSubmit 函数作为回调来执行,并将收集到的、结构化的表单数据 ({ username: '...', email: '...' }) 作为第一个参数传入。

    • 如果校验失败,它不会调用 onSubmit,而是会自动更新 formState.errors 对象,此时由于 errors 对象发生了变化,React 组件会进行一次渲染,从而将错误信息显示在界面上。

4.formState: { errors }

这是一个包含了表单当前状态的对象,里面还有更多信息,比如 isDirty (是否被修改过), isValid (是否有效), 当然还有我们用到的 errors (错误信息)。

1
2
3
4
5
6
7
formState: {
errors: Object,
isDirty: boolean,
isValid: boolean,
isSubmitting: boolean,
... // 还有很多
},

但是通常情况,我们只用到了 errors 。

这个对象是响应式的,当它的状态改变时,会触发组件更新。因为这个原因,我们可以使用这个解构出的 errors,来在 UI 上呈现。

解构出的其他工具

上面只说了四个常用的,我会在这里补充其他我用过的工具的注意点。

setValue(name, value, options?)

setValue 能单点更新某个值,而不会默认重置整个表单的提交/脏/计数等全局状态,这一点就和下面要说的 reset 不同了。

  • 用途:程序化地设置单个字段的值(例如:根据另一个字段计算结果、外部数据更改、第三方控件回调等)。

  • 常用选项(第三参):{ shouldValidate?: boolean, shouldDirty?: boolean, shouldTouch?: boolean }

    • shouldValidate 为 true 会触发字段验证;

    • shouldDirty 控制是否将字段标记为 dirty;

    • shouldTouch 控制是否将字段标记为 touched。

  • 注意:通常 setValue 只影响指定字段,。适合“单点更新”。示例:

1
2
// 计算 total 并更新(同时触发验证)
setValue("total", price * qty, { shouldValidate: true, shouldDirty: true });

reset(values?, options?)

  • 用途:重置整个表单的值与状态(默认会把 values 替换为传入值或 defaultValues,并且清除/重置 formState,如 isDirtyisSubmitSuccessfulsubmitCount 等)。通常用于:提交成功后清空表单、把远端载入的数据填入表单(把那套数据当作新“初始值”)等。

  • 选项reset 有一组 keep* 选项,可以选择保留 某些状态(例如 keepValueskeepErrorskeepDirtykeepTouchedkeepIsValidkeepSubmitCount 等),以便细粒度控制(比如想保留错误但重置值,或保留值但重置 dirty 状态等)。示例:

1
2
3
4
5
6
7
8
// 提交后,重置为空表单(清除所有状态)
reset();

// 把远端数据填入并把它当成 default(常用于 edit 表单)
reset(fetchedData);

// 想保持当前值但重置 isDirty(示例)
reset(undefined, { keepValues: true, keepDirty: false });
关于输入后清空表单的问题

有一个常见的需求,是在用户提交表单后,清空之前填入的内容。如果提交失败,则不清空。
理所当然的,我们会想着直接用 reset() 而不传参,但是在部分情况下,这有时会导致 bug。
如果你之前在某个地方,已经用过 reset 但是传参,那么此时程序把它当成 default,而不再使用先前设定的 defauleValue 了。
为了解决这个 bug,建议固定使用 reset(defaultValue) 来在成功的时候清空表单,defaultValue 就是你手动设置的那个表单初始值,大部分情况下为空。
例如,在定义的时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const {
    register,
    handleSubmit,
    formState: { errors },
    setError,
    reset,
  } = useForm<todoSchemaType>({
    resolver: zodResolver(todoSchema),
    mode: "onBlur", // 失焦触发校验
    reValidateMode: "onChange", // 更改时重新校验
    defaultValues: {
      content: "",
    },
  });

那么下面你想清空表单的时候,使用

1
2
3
 reset({
content: "",
});

resetField(name, options?)

如果你只想重置单个字段(恢复到 defaultValues 或清空该字段的状态),可以用 resetField("x"),它是对单字段的重置工具(区别于 reset() 的全表重置)

  • 标题: 使用 React Hook Form 现代、高效的处理表单
  • 作者: 三葉Leaves
  • 创建于 : 2025-09-05 00:00:00
  • 更新于 : 2025-09-05 12:58:10
  • 链接: https://blog.oksanye.com/77abdd80c633/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论