Skip to content

useForm

React hooks for form validation

useForm: UseFormProps

useForm is a custom hook for managing forms with ease. It takes one object as optional argument. The following example demonstrates all of its properties along with their default values.

function App() {
  const methods = useForm();
  
  return null;
}type FormInputs = {
  firstName: string;
  lastName: string;
};

const { register } = useForm<FormInputs>();

// type inference with defaultValues
const { register } = useForm({
  defaultValues: {
    firstName: '',
    lastName: ''
  }
});

Props

mode: onChange | onBlur | onSubmit | onTouched | all = 'onSubmit'React Native: compatible with Controller

This option allows you to configure the validation strategy before a user submits the form that is happened during onSubmit event by invoking handleSubmit function.

NameTypeDescription
onSubmitstringValidation will trigger on the submit event and inputs will attach onChange event listeners to re-validate them.
onBlurstringValidation will trigger on the blur event.
onChangestringValidation will trigger on the change event with each input, and lead to multiple re-renders. Warning: this often comes with a significant impact on performance.
onTouchedstring

Validation will trigger on the first blur event. After that, it will trigger on every change event.

Note: when using with Controller, make sure to wire up onBlur with the render prop.

allstringValidation will trigger on the blur and change events.
reValidateMode: onChange | onBlur | onSubmit = 'onChange'React Native: Custom register or using Controller

This option allows you to configure validation strategy when inputs with errors get re-validated after a user submits the form (onSubmit event and handleSubmit function executed). By default, re-validation is actioned during the input change event.

defaultValues: FieldValues | Promise<FieldValues>

The defaultValues prop is used to populate the entire form values. It supports both sync and async set form default values. You can set an input's default value with defaultValue/defaultChecked (read more from the official React doc), but it is encouraged that you set defaultValues for the entire form.

// set default value sync
useForm({
  defaultValues: {
    firstName: '',
    lastName: ''
  }
})

// set default value async
useForm({
  defaultValues: async () => fetch('/api-endpoint');
})

Rules

  • You should provide none undefined default value, as undefined value is conflicting with controlled component as default state.

  • defaultValues are cached. If you want to reset the defaultValues, you should use the reset api.

  • defaultValues will be included in the submission result by default.

  • It's recommend to avoid including custom object which contains prototype methods as the defaultValues, such as moment, luxon and etc.

  • There are other options to include form data:

    // include hidden input
    <input {...register("hidden")} type="hidden" />
    register("hidden", { value: "data" })
    
    // include data onSubmit
    const onSubmit = (data) => {
      const output = {
        ...data,
        others: "others"
      }
    }
    
values: FieldValues

The values props will react to changes and update the form values, which is useful when your form needs to be updated by external state or server data.

// set default value sync
function App({ values }) {
  useForm({
    values  // will get updated when values props updates       
  })
}

function App() {
  const values = useFetch('/api');
  
  useForm({
    defaultValues: {
      firstName: '',
      lastName: '',
    },
    values, // will get updated once values returns
  })
}
resetOptions: KeepStateOptions

This property is associated with value update behaviours. In fact, values or defaultValues update will invoke the reset API internally. So it's important to info what behaviour should be after the values or defaultValues get asynchronously updated. The config option itself is a reference to reset method's options.

// by default asynchronously value or defaultValues update will reset the form values
useForm({ values })
useForm({ defaultValues: async () => await fetch() })

// options to config the behaviour
// eg: I want to keep user interacted/dirty value and not remove any user errors
useForm({
  values,
  resetOptions: {
    keepDirtyValues: true, // user-interacted input will be retained
    keepErrors: true, // input errors will be retained with value update
  }
})
context: object

This context object is mutable and will be injected into the resolver's second argument or Yup validation's context object.

CodeSandbox
criteriaMode: firstError | all
  • When set to firstError (default), only the first error from each field will be gathered.

  • When set to all, all errors from each field will be gathered.

CodeSandbox
shouldFocusError: boolean = true

When set to true (default) and the user submits a form that fails the validation, it will set focus on the first field with an error.

Note: only registered fields with a ref will work. Custom registered inputs do not apply. For example: register('test') // doesn't work

Note: the focus order is based on the register order.

delayError: number

This config will delay the error state to be displayed to the end-user in milliseconds. Correct the error input will remove the error instantly and delay will not be applied.

CodeSandbox
shouldUnregister: boolean = false

By default, an input value will be retained when input is removed. However, you can set shouldUnregister to true to unregister input during unmount.

  • This is a global config that overwrites child-level config, if you want to have individual behavior, then you should set the config at the component or hook level, not at useForm.

  • By default shouldUnregister: false: unmounted fields will not be validated by build-in validation.

  • By setting shouldUnregister to true at useForm level, defaultValues will not be merged against submission result.

  • set shouldUnregister: true will set your form behave more closer as native.

    Form values will be lived inside your inputs itself.

    • input unmount will remove value.

    • input hidden should be applied for hidden data.

    • only registered input will be included as submission data.

    • unmounted input will need to notify at either useForm, or useWatch's useEffect for hook form to verify input is unmounted from the DOM.

      const NotWork = () => {
        const [show, setShow] = React.useState(false);
        // ❌ won't get notified, need to invoke unregister
        return {show && <input {...register('test')} />}
      }
      
      const Work = ({ control }) => {
        const { show } = useWatch({ control })
        // ✅ get notified at useEffect
        return {show && <input {...register('test1')} />}
      }
      
      const App = () => {
        const [show, setShow] = React.useState(false);
        const { control } = useForm({ shouldUnregister: true });
        return (
          <div>
            // ✅ get notified at useForm's useEffect
            {show && <input {...register('test2')} />}
            <NotWork />
            <Work control={control} />
          </div>
        )
      }
      
shouldUseNativeValidation: boolean = false

This config will enable browser native validation. It will also enable CSS selectors :valid and:invalid making style inputs easier. In fact, you can still use those selectors even the client validation is disabled.

  • Only works with onSubmit and onChange mode due to reportValidity execution will focus on the error input.
  • Each registered field's validation message is required to be string to display them natively.
  • This feature only works for register API, and useController/Controller which wired with actual DOM reference.

Examples

import { useForm } from "react-hook-form";

export default function App() {
  const { register, handleSubmit } = useForm({ shouldUseNativeValidation: true });
  const onSubmit = async data => { console.log(data); };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register("firstName", { required: "Please enter your first name." })} // custom message
      />
      <input type="submit" />
    </form>
  );
}
resolver: Resolver

This function allows you to use any external validation library such as Yup, Zod, Joi, Vest, Ajv and many others. The goal is to make sure you can seamlessly integrate whichever validation library you prefer. If you're not using a library, you can always write your own logic to validate your forms.

npm install @hookform/resolvers

Props

NameTypeDescription

values

object

This object contains the entire form values.

context

object

This is the context object which you can provide to the useForm config. It is a mutable object that can be changed on each re-render.

options

{ criteriaMode: string, fields: object, names: string[] }

This is the option object contains information about the validated fields, names and criteriaMode from useForm.

Rules

  • Schema validation focus on the field level for error reporting. Parent level error look is only limited to the direct parent level that is applicable for components such as group checkboxes.

  • This function will be cached.

  • Re-validation of an input will only occur one field at time during a user’s interaction. The lib itself will evaluate the error object to trigger a re-render accordingly.

  • A resolver can not be used with the built-in validators (e.g.: required, min, etc.)

  • When building a custom resolver:

    • Make sure you are returning an object that has both a values and an errors property. Their default values should be an empty object. For example: {}.

    • The keys of the error object should match the name values of your fields.

Examples

import React from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from "yup";

const schema = yup.object().shape({
  name: yup.string().required(),
  age: yup.number().required(),
}).required();

const App = () => {
  const { register, handleSubmit } = useForm({
    resolver: yupResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit(d => console.log(d))}>
      <input {...register("name")} />
      <input type="number" {...register("age")} />
      <input type="submit" />
    </form>
  );
};import React from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from "yup";

type Inputs = {
  name: string;
  age: string;
};

const schema = yup.object().shape({
  name: yup.string().required(),
  age: yup.number().required(),
}).required();

const App = () => {
  const { register, handleSubmit } = useForm<Inputs>({
    resolver: yupResolver(schema), // yup, joi and even your own.
  });

  return (
    <form onSubmit={handleSubmit(d => console.log(d))}>
      <input {...register("name")} />
      <input type="number" {...register("age")} />
      <input type="submit" />
    </form>
  );
};
import React from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";

const schema = z.object({
  name: z.string(),
  age: z.number()
});

const App = () => {
  const { register, handleSubmit } = useForm({
    resolver: zodResolver(schema)
  });

  return (
    <form onSubmit={handleSubmit(d => console.log(d))}>
      <input {...register("name")} />
      <input {...register("age")} type="number" />
      <input type="submit" />
    </form>
  );
};
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";

const schema = z.object({
  name: z.string(),
  age: z.number()
});

type Schema = z.infer<typeof schema>;

const App = () => {
  const { register, handleSubmit } = useForm<Schema>({
    resolver: zodResolver(schema)
  });

  const onSubmit = (data: Schema) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} />
      <input {...register("age")} type="number" />
      <input type="submit" />
    </form>
  );
};
import React from 'react';
import { useForm } from 'react-hook-form';
import { joiResolver } from '@hookform/resolvers/joi';
import Joi from "joi";

const schema = Joi.object({
  name: Joi.string().required(),
  age: Joi.string().required(),
});

const App = () => {
  const { register, handleSubmit } = useForm({
    resolver: joiResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit(d => console.log(d))}>
      <input {...register("name")} />
      <input type="number" {...register("age")} />
      <input type="submit" />
    </form>
  );
};
import React from "react";
import { useForm } from "react-hook-form";
import { joiResolver } from "@hookform/resolvers/joi";
import Joi from "joi";

interface IFormInput {
  name: string;
  age: number;
}

const schema = Joi.object({
  name: Joi.string().required(),
  age: Joi.number().required()
});

const App = () => {
  const { register, handleSubmit, formState: { errors } } = useForm<IFormInput>({
    resolver: joiResolver(schema)
  });
  const onSubmit = (data: IFormInput) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name"} />
      <input type="number" {...register("age"} />
      <input type="submit" />
    </form>
  );
}
import { useForm } from 'react-hook-form';
import { ajvResolver } from '@hookform/resolvers/ajv';

// must use `minLength: 1` to implement required field
const schema = {
  type: 'object',
  properties: {
    username: {
      type: 'string',
      minLength: 1,
      errorMessage: { minLength: 'username field is required' },
    },
    password: {
      type: 'string',
      minLength: 1,
      errorMessage: { minLength: 'password field is required' },
    },
  },
  required: ['username', 'password'],
  additionalProperties: false,
};

const App = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: ajvResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register('username')} />
      {errors.username && <p>{errors.username.message}</p>}
      <input {...register('password')} />
      {errors.password && <p>{errors.password.message}</p>}
      <button type="submit">submit</button>
    </form>
  );
};
import * as React from 'react';
import { useForm } from 'react-hook-form';
import { vestResolver } from '@hookform/resolvers/vest';
import vest, { test, enforce } from 'vest';

const validationSuite = vest.create((data = {}) => {
  test('username', 'Username is required', () => {
    enforce(data.username).isNotEmpty();
  });

  test('username', 'Must be longer than 3 chars', () => {
    enforce(data.username).longerThan(3);
  });

  test('password', 'Password is required', () => {
    enforce(data.password).isNotEmpty();
  });

  test('password', 'Password must be at least 5 chars', () => {
    enforce(data.password).longerThanOrEquals(5);
  });

  test('password', 'Password must contain a digit', () => {
    enforce(data.password).matches(/[0-9]/);
  });

  test('password', 'Password must contain a symbol', () => {
    enforce(data.password).matches(/[^A-Za-z0-9]/);
  });
});

const App = () => {
  const { register, handleSubmit } = useForm({
    resolver: vestResolver(validationSuite),
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register("username")} />
      <input {...register("password")} />
      <input type="submit" />
    </form>
  );
};
import * as React from "react";
import { useForm } from "react-hook-form";
import * as Joi from "joi";

const validationSchema = Joi.object({
  username: Joi.string()
    .alphanum()
    .min(3)
    .max(30)
    .required()
});

const App = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    context: "context",
    resolver: async (data, context) => {
      const { error, value: values } = validationSchema.validate(data, {
        abortEarly: false,
      });

      if (!error) return { values: values, errors: {} };

      return {
        values: {},
        errors: error.details.reduce(
          (previous, currentError) => ({
            ...previous,
            [currentError.path[0]]: currentError,
          }),
          {},
        ),
      };
    },
  });

  const onSubmit = data => {
    console.log(data)
  };

  return (
    <div className="App">
      <h1>resolver</h1>
      
      <form onSubmit={handleSubmit(onSubmit)}>
        <label>Username</label>
        <input {...register("username")} />
        {errors.username && <p>errors.username.message</p>}
        <input type="submit" />
      </form>
    </div>
  );
};
import * as React from "react";
import { useForm } from "react-hook-form";
import * as Joi from "joi";

interface IFormInputs {
  username: string;
}

const validationSchema = Joi.object({
  username: Joi.string()
    .alphanum()
    .min(3)
    .max(30)
    .required()
});

const App = () => {
  const { register, handleSubmit, formState: { errors } } = useForm<IFormInputs>({
    resolver: async data => {
      const { error, value: values } = validationSchema.validate(data, {
        abortEarly: false
      });

      return {
        values: error ? {} : values,
        errors: error
          ? error.details.reduce((previous, currentError) => {
              return {
                ...previous,
                [currentError.path[0]]: currentError
              };
            }, {})
          : {}
      };
    }
  });

  const onSubmit = (data: IFormInputs) => console.log(data);

  return (
    <div className="App">
      <h1>resolver</h1>

      <form onSubmit={handleSubmit(onSubmit)}>
        <label>Username</label>
        <input {...register("username")} />
        {errors.username && <p>errors.username.message</p>}
        <input type="submit" />
      </form>
    </div>
  );
};

Need more? See Resolver Documentation

Tip

You can debug your schema via the following code snippet:

resolver: async (data, context, options) => {
  // you can debug your validation schema here
  console.log('formData', data)
  console.log('validation result', await anyResolver(schema)(data, context, options))
  return anyResolver(schema)(data, context, options)
},

Return

The following list contains reference to useForm return props.

Thank you for your support

If you find React Hook Form to be useful in your project, please consider to star and support it.

Edit