React Hook Form使用指南
表单作为用户交互中常见形式,值的验证/状态管理是个问题。
在react技术栈下中React Hook Form
包名: react-hook-form
功能强大,对于复杂表单管理,是个不错的方案。 为了能灵活恰当的使用它,这里把常用场景/使用/容易忽略的点/核心原理做一个总结,如有疏漏,请斧正。
使用🔨
实际使用中,我们常用的可能只是useForm/Controller/getValues
,hook-form还有其他一些方法/配置,这里就介绍下。
useForm中的mode
mode不同,影响表单校验的时机,比如validate之后的errors获取,会有所不同。。
mode缺省值是onSubmit
export const VALIDATION_MODE = {
onBlur: 'onBlur',
onChange: 'onChange',
onSubmit: 'onSubmit',
onTouched: 'onTouched',
all: 'all',
} as const;
注意:mode影响的验证策略,form一直会感知到值得变化,Validation strategy before submitting behaviour.
reValidateMode
注意:如果mode本身配置了onChange,那么reValidateMode没意义。
useFormContext
hook
,如果form是一个N级的组件树,那么这样复杂嵌套的表单项的管理和form控制参数传递就成了问题。我们需要将create form创建的的control/setValue等等传递给子组件,子组件再传递给下级组件,一层一层传递,非常麻烦。
如果是使用useFormContext,那么就可以直接在组件内部管理form的值,而不需要一层一层传递。
层层传递

Context
<FormProvider {...formProps}>
...
</FormProvider>
const {control} = useFormContext();
useFieldArray
hook
,有时数据存在数组/List类型。比如动态添加项,使用useFieldArray会更为方便。
const { fields, append, prepend, remove, swap, move, insert } = useFieldArray(
{
control, // control props comes from useForm (optional: if you are using FormProvider)
name: 'test' // unique name for your Field Array
}
);
注意:不使用useFieldArray仍然可以使用数组,只是不够方便
<Form.Item label={'Person 0'}>
<input
{...register('persons.0', {
})}
/>
</Form.Item>
watch
watchfunc
,useWatch用来监听关心的字段变化,或者所有字段当前的值,但假如想知道每次变化哪个字段,或者批量watch用于逻辑处理的话,可以使用watch方法。
useEffect(() => {
const wFn = watch((data, {name}) => {
console.log('column changed', data, name) // data为修改后最新值集合
});
return wFn.unsubscribe;
}, [])
注意:订阅时需要注意需要取消订阅。
watch vs useWatch
react-hook-form中有两个watch方法,一个是useWatch,一个是watch。useWatch是hook方法,watch是函数方法。在针对单个字段的订阅,可以使用watch,也可以使用useWatch,但推荐使用useWatch。WHY ?
const allFieldWatch = useWatch({
control,
name:['price'], // 如果name不传,则是watch所有字段
})
const priceWatch = watch(['price']);
register
registerfunc
,注册一个表单字段,返回对象,该方法主要是面向原生表单元素的,如果是非原生表单元素,比如Tea组件下的Input组件,注意onChange等方法是否会有问题。
<input
{...register('address', {
validate: (s) => {
console.log('address validate', s);
}
})}
/>
注意:
- name支持嵌套写法,比如
persons.0
,address.city
。 - 针对数组,是可以直接以非0下标开始注册。
register vs Controller
如上所说,register适合原生,Controller适合高阶表单组件,如第三方。
<Form.Item label={'Person 0'}>
<input
{...register('persons.0', {
// valueAsNumber: true,
})}
/>
</Form.Item>
<Form.Item label={'Person 1'}>
<Input
{...register('persons.1', {
// valueAsNumber: true,
})}
/>
</Form.Item>
PS: 官方更提倡更提倡使用register
。
Controller vs useController
Controller本质即调用的useController,因此一样,仅仅使用方式的不同,一个组件标签,一个hook函数。
const Controller = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
TTransformedValues = TFieldValues,
>(
props: ControllerProps<TFieldValues, TName, TTransformedValues>,
) =>
props.render(useController<TFieldValues, TName, TTransformedValues>(props));
export { Controller };
resetfunc
/ setValue
reset方法如果没有声明字段值,则会重置为defaultValues设置的值。
reset();
reset操作是面向所有字段进行重置,并非部分字段。比如reset只标注了A字段,那么其它字段则会是undefined.
如果想reset单个字段,应该使用setValue法或者reset时候携带所有字段值
reset({ ...getValues(), price: 111 })
注意:有setValue,而没有setValues,因此setValue实现多值,只能自己遍历处理
shouldDirty
setValue时候的第二个参数中有shouldDirty,表示是否需要设置dirty状态。当设置为true时,form会去校验dirty状态,从而确定更新dirtyFields。比如如果跟defaultValues对比没有变化,那么dirtyFields就会有值。
注意:reset之后,defaultValues可能会被更新为新设置的值。
valueAsNumber/valueAsDate
<input
{...register('quantity', {
// valueAsNumber: true,
})}
/>
// "quantity": "2121212121"
// "quantity": 2121212121
如果是Controller方式,需要自行实现,比如field.onChange(Number(value))
dirtyFields
formState中有dirtyFields,表示当前表单中被修改过的字段。当需要判断表单哪些字段被修改过时,可以使用这个属性。
```shell
const {isDirty, dirtyFields} = useFormState({
control
});
官方推荐-实践👊
transform/parse
react-hook-form介绍的transform和parse只是个实践,并不是内置的功能。
const form_fields = [
{
name: 'price',
transform: {
output: v => +v
}
},
{
name: 'num'
},
{
name: 'quantity'
}
]
{
form_fields.map(item => <Form.Item label={item.name}>
<Controller key={item.name} render={({field}) => <Input {...field}
onChange={v => {
return field.onChange(item?.transform?.parse ? item.transform.output(v) : v);
}}
/>} name={item.name}
control={control}/>
</Form.Item>)
}
配套-yup等校验类库
常见的类库有 yup/Joi/Superstruct/zod。
const schema = yup
.object()
.shape({
price: yup.number().required(),
quantity: yup.number().min(1).max(100).required(),
})
.required();
const formProps = useForm({
...,
resolver: yupResolver(schema),
});
注意:触发校验还是取决于mode的设置。
常见问题❓
setValue 值为null
当我们在设置值为null/undefined时,受控表单组件不会正常渲染更新。
这点是由于react对于受控组件处理机制,而非hook-form的特殊处理。
setValue('price', null, {
shouldDirty: true,
});
const [inputValue, setInputValue] = useState(1_0);
<div>
<label>
<span>姓名</span>
<input value={inputValue}/>
</label>
<button onClick={() => {
setInputValue(5);
}}>
设置为空
</button>
</div>
解决办法
比如如下,在render时处理下。
<input value={inputValue??''}/>
源码👀
这里通过阅读hook-form的部分源码,解答一些问题。
包依赖 - zero dependency
react-hook-form没有dependency,只有peerDependencies。因此只要是react项目,就可以正常使用react-hook-form???
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19"
},
比如微信小程序开发框架taro是使用的react进行开发,那么如果需要表单校验的话,可以使用react-hook-form吗?
答案:YES
。
一段taro小程序代码如下,UI表单组件使用的taro-ui
const { control } = useForm({
mode: 'onChange',
});
...
<Form>
<Controller
control={control}
name="name"
render={({ field }) => <AtInput {...field} title="Name"/>}
/>
<Controller
control={control}
name="test"
render={({ field }) => <AtInput {...field} title="Test Input"/>}
/>
</Form>
resolver原理
- react hook form之所以能够跟不同的schema验证库对接,是因为单独有个包 - @hookform/resolvers 对各个库做了对应的适配支持。

...
if (result.error) {
return {
values: {},
errors: toNestErrors(
parseErrorSchema(
result.error,
!options.shouldUseNativeValidation &&
options.criteriaMode === 'all',
),
options,
),
};
}
...
性能好?
- 避免不必要的re-render。
- 使用原生表单注册机制 + ref 管理。
Controller
实现受控组件的最小渲染。- 按需解构状态,而非一次性全部读取。
useWatch原理
本身是在hook的effect中调用了form.control的subscribe方法,来监听表单值的变化,一旦值变动了,则会更新value对象。
export function useWatch<TFieldValues extends FieldValues>(
props?: UseWatchProps<TFieldValues>,
) {
...
React.useEffect(
() =>
control._subscribe({
name: _name.current as InternalFieldName,
...
callback: (formState) =>
!disabled &&
updateValue(
generateWatchOutput(
_name.current as InternalFieldName | InternalFieldName[],
control._names,
formState.values || control._formValues,
false,
_defaultValue.current,
),
),
}),
[control, disabled, exact],
);
const [value, updateValue] = React.useState(
control._getWatch(
name as InternalFieldName,
defaultValue as DeepPartialSkipArrayKey<TFieldValues>,
),
);
...
return value;
}
补充信息🔗
react-hook-form什么时候推出的
查询发现,react-hook-form于Mar 3, 2019发布的第一个版本,目前也属于高频维护。
包大小
当前版本:v7.56.3
,GZIP压缩使用的包体积为11KB左右
。
支持接入第三方UI-表单组件
比如tea-component、mui、antd。
其它form方案选择
相关链接
- https://react-hook-form.com/
- https://github.com/orgs/react-hook-form/discussions/2704
- https://www.reddit.com/r/react/comments/1ed4sdj/controller_wrapper_vs_register_utility_function/
- https://npmtrends.com/formik-vs-react-final-form-vs-react-hook-form
- https://github.com/react-hook-form/react-hook-form/pull/4825
- https://juejin.cn/post/7423261843933675558