React Hook Form Usage Guide

· 7 min read

Forms are a common interaction method in user interfaces, and validating/managing their state can be challenging.

In the React ecosystem, React Hook Form package name: react-hook-form is a powerful solution for complex form management. To use it flexibly and appropriately, this guide summarizes common scenarios, usage patterns, easily overlooked points, and core principles. Please provide feedback if there are any omissions.

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

Usage🔨

In practical use, we often only use useForm/Controller/getValues, but hook-form has other methods/configurations that are worth exploring.

mode in useForm

Different modes affect when form validation occurs, such as when errors are obtained after validation.

The default mode is onSubmit

export const VALIDATION_MODE = {
  onBlur: 'onBlur',
  onChange: 'onChange',
  onSubmit: 'onSubmit',
  onTouched: 'onTouched',
  all: 'all',
} as const;

Note: mode affects validation strategy, but the form always detects value changes. Validation strategy before submitting behaviour.

reValidateMode

Note: If mode itself is configured as onChange, then reValidateMode becomes meaningless.

useFormContext

hook - If a form is an N-level component tree, managing complex nested form items and passing form control parameters becomes problematic. We need to pass control/setValue etc. created by create form to child components, which then pass them down to lower-level components, creating a cumbersome chain. If using useFormContext, you can directly manage form values within components without needing to pass them down layer by layer.

Layer-by-layer passing

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

Context

<FormProvider {...formProps}>
...
</FormProvider>


const {control} = useFormContext();

useFieldArray

hook - Sometimes data exists in array/List format. For dynamic item addition, useFieldArray is more convenient.

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

Note: You can still use arrays without useFieldArray, but it’s less convenient


<Form.Item label={'Person 0'}>
              <input
                {...register('persons.0', {
                })}
              />
            </Form.Item>

watch

watchfunc - useWatch is used to monitor specific field changes or all current field values, but if you want to know which field changed each time or perform batch watching for logic processing, you can use the watch method.

useEffect(() => {
    const wFn = watch((data, {name}) => {
      console.log('column changed', data, name) // data contains the latest values after modification
    });
    return wFn.unsubscribe;
  }, [])

Note: Remember to unsubscribe when subscribing.

watch vs useWatch

React-hook-form has two watch methods: useWatch and watch. useWatch is a hook method, watch is a function method. For single field subscriptions, you can use either watch or useWatch, but useWatch is recommended. WHY?

const allFieldWatch = useWatch({
    control,
    name:['price'], // If name is not provided, it watches all fields
  })
const priceWatch = watch(['price']);

register

registerfunc - Registers a form field and returns an object. This method is primarily for native form elements. For non-native form elements like Input components in Tea component libraries, be cautious about potential issues with onChange methods.

              <input
                {...register('address', {
                  validate: (s) => {
                    console.log('address validate', s);
                  }
                })}
              />

Note:

  1. name supports nested notation, such as persons.0, address.city.
  2. For arrays, you can directly register with non-zero indices.

register vs Controller

As mentioned above, register is suitable for native elements, while Controller is suitable for higher-order form components, such as third-party components.

<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: The official documentation prefers using register.

Controller vs useController

Controller essentially calls useController, so they are the same - just different usage patterns: one is a component tag, the other is a hook function.

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. If reset method doesn’t specify field values, it resets to the values set in defaultValues.

    reset();
    
  2. reset operation resets all fields, not just specific ones. For example, if reset only specifies field A, other fields will become undefined.

  3. To reset a single field, you should use setValue method or carry all field values when resetting

     reset({
     ...getValues(),
     price: 111
    })
    

Note: There is setValue but no setValues, so to implement multiple values with setValue, you need to handle it yourself through iteration

shouldDirty

The second parameter in setValue has shouldDirty, indicating whether to set the dirty state. When set to true, the form will check the dirty state to determine updates to dirtyFields. For example, if there’s no change compared to defaultValues, dirtyFields will have values.

Note: After reset, defaultValues may be updated to the newly set values.

valueAsNumber/valueAsDate

              <input
                {...register('quantity', {
                  // valueAsNumber: true,
                })}
              />


//  "quantity": "2121212121"
//  "quantity": 2121212121

If using Controller approach, you need to implement it yourself, such as field.onChange(Number(value))

dirtyFields

formState contains dirtyFields, indicating which fields in the current form have been modified. Use this property when you need to determine which form fields have been changed.

```shell
  const {isDirty, dirtyFields} = useFormState({
    control
  });

Official Recommendations - Practice👊

transform/parse

transform and parse mentioned in react-hook-form are just practices, not built-in functionality.

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

Integration with validation libraries like yup

Common libraries include 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),
});

Note: Validation triggering still depends on the mode setting.

Common Issues❓

setValue with null values

When setting values to null/undefined, controlled form components may not render updates properly.

This is due to React’s controlled component handling mechanism, not special handling by hook-form.

setValue('price', null, {
                  shouldDirty: true,
});
const [inputValue, setInputValue] = useState(1_0);
<div>
        <label>
          <span>Name</span>
          <input value={inputValue}/>
        </label>
        <button onClick={() => {
          setInputValue(5);
        }}>
          Set to empty
        </button>
      </div>

Solution

For example, handle it during rendering like this.

<input value={inputValue??''}/>

Source Code👀

Here we examine some parts of hook-form’s source code to answer certain questions.

Package dependencies - zero dependency

React-hook-form has no dependencies, only peerDependencies. So can any React project use react-hook-form normally???

"peerDependencies": {
    "react": "^16.8.0 || ^17 || ^18 || ^19"
  },

For example, the WeChat mini-program development framework taro uses React for development. Can react-hook-form be used for form validation if needed?

Answer: YES.

Sample Taro mini-program code, using taro-ui for UI form components:

  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 principle

  1. React hook form can integrate with different schema validation libraries because there’s a separate package - @hookform/resolvers that provides adapter support for various libraries.
https://static.1991421.cn/2025/2025-05-14-111141.jpeg 2. The resolver validates based on formState values combined with the schema, then returns error information to the form in the agreed format.
...    
if (result.error) {
      return {
        values: {},
        errors: toNestErrors(
          parseErrorSchema(
            result.error,
            !options.shouldUseNativeValidation &&
              options.criteriaMode === 'all',
          ),
          options,
        ),
      };
    }
...

Good performance?

  1. Avoid unnecessary re-renders.
  2. Use native form registration mechanism + ref management.
  3. Controller implements minimal rendering for controlled components.
  4. Destructure state as needed, rather than reading everything at once.

useWatch principle

Essentially calls form.control’s subscribe method in the hook’s effect to monitor form value changes. Once values change, it updates the value object.

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

Additional Information🔗

When was react-hook-form released?

Research shows that react-hook-form released its first version on Mar 3, 2019 and is currently actively maintained.

Package size

Current version: v7.56.3, with GZIP compressed package size of approximately 11KB.

Support for third-party UI form components

Such as tea-component, mui, antd.

Other form solution options

Final Thoughts

React Hook Form provides an excellent solution for form management in React applications, offering performance optimizations, flexibility, and a clean API. Its zero-dependency approach and small bundle size make it suitable for various projects, from web applications to mini-programs. By understanding its core concepts like validation modes, field registration, and state management, developers can build robust and efficient forms that provide great user experiences.