React Hook Form使用指南

· 7 min read

表单作为用户交互中常见形式,值的验证/状态管理是个问题。

在react技术栈下中React Hook Form包名: react-hook-form功能强大,对于复杂表单管理,是个不错的方案。 为了能灵活恰当的使用它,这里把常用场景/使用/容易忽略的点/核心原理做一个总结,如有疏漏,请斧正。

https://static.1991421.cn/2025/2025-03-31-142653.jpeg

使用🔨

实际使用中,我们常用的可能只是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的值,而不需要一层一层传递。

层层传递

https://static.1991421.cn/2025/2025-05-13-165717.jpeg

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);
                  }
                })}
              />

注意:

  1. name支持嵌套写法,比如persons.0address.city
  2. 针对数组,是可以直接以非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

  1. reset方法如果没有声明字段值,则会重置为defaultValues设置的值。

    reset();
    
  2. reset操作是面向所有字段进行重置,并非部分字段。比如reset只标注了A字段,那么其它字段则会是undefined.

  3. 如果想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只是个实践,并不是内置的功能。

https://static.1991421.cn/2025/2025-05-13-225028.jpeg

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原理

  1. react hook form之所以能够跟不同的schema验证库对接,是因为单独有个包 - @hookform/resolvers 对各个库做了对应的适配支持。
https://static.1991421.cn/2025/2025-05-14-111141.jpeg 2. resolver中根据formState的值结合schema进行校验,之后按照约定返回格式错误信息给form。
...    
if (result.error) {
      return {
        values: {},
        errors: toNestErrors(
          parseErrorSchema(
            result.error,
            !options.shouldUseNativeValidation &&
              options.criteriaMode === 'all',
          ),
          options,
        ),
      };
    }
...

性能好?

  1. 避免不必要的re-render。
  2. 使用原生表单注册机制 + ref 管理。
  3. Controller 实现受控组件的最小渲染。
  4. 按需解构状态,而非一次性全部读取。

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-componentmuiantd

其它form方案选择

相关链接