Editor’s note: This article was last updated by Jude Miracle on 23 July 2024 to cover how to use the useFormContext Hook in React Hook Form, handle complex data structures like nested fields and arrays within forms, and update code blocks where necessary.

Forms are an essential part of how users interact with websites and web applications. Validating a user’s data passed through a form is a crucial responsibility for a developer.
React Hook Form is a library that helps validate forms in React. It is a minimal library without any other dependencies, and is performant and straightforward to use, requiring developers to write fewer lines of code than other form libraries.
In this guide, you will learn how to use the React Hook Form library to build forms in React without using any complicated render props or higher-order components.
React Hook Form takes a slightly different approach than other form libraries in the React ecosystem by using uncontrolled inputs with ref instead of depending on the state to control the inputs. This approach makes the forms more performant and reduces the number of re-renders. This also means that React Hook Form offers seamless integration with UI libraries because most libraries support the ref attribute.
React Hook Form’s size is tiny (just 8.6 kB minified and gzipped) and it has zero dependencies. The API is very intuitive, which provides a seamless experience to developers. The library follows HTML standards for validating the forms using a constraint-based validation API.
To install React Hook Form, run the following command:
npm install react-hook-form
In this section, you will learn about the fundamentals of the useForm Hook by creating a very basic registration form.
First, import the useForm Hook from the react-hook-form package:
import { useForm } from "react-hook-form";
Then, inside your component, use the Hook as follows:
const { register, handleSubmit } = useForm();
The useForm Hook returns an object containing a few properties. For now, we’ll only require register and handleSubmit.
The register method helps you register an input field into React Hook Form so that it is available for validation, and its value can be tracked for changes.
To register the input, we’ll pass the register method into the input field as such:
<input type="text" name="firstName" {...register('firstName')} />
This spread operator syntax is a new implementation to the library that enables strict type checking in forms with TypeScript. You can learn more about strict type checking in React Hook Form here.
React Hook Form versions older than v7 had the register method attached to the ref attribute as such:
<input type="text" name="firstName" ref={register} />
Note that the input component must have a name prop, and its value should be unique. The handleSubmit method, as the name suggests, manages form submission. It needs to be passed as the value to the onSubmit prop of the form component.
The handleSubmit method can handle two functions as arguments. The first function passed as an argument will be invoked along with the registered field values when the form validation is successful. The second function is called with errors when the validation fails:
const onFormSubmit = data => console.log(data);
const onErrors = errors => console.error(errors);
<form onSubmit={handleSubmit(onFormSubmit, onErrors)}>
{/* ... */}
</form>
Now that you have a fair idea about the basic usage of the useForm Hook, let’s take a look at a more realistic example:
import React from "react";
import { useForm } from "react-hook-form";
const RegisterForm = () => {
const { register, handleSubmit } = useForm();
const handleRegistration = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(handleRegistration)}>
<div>
<label>Name</label>
<input name="name" {...register('name')} />
</div>
<div>
<label>Email</label>
<input type="email" name="email" {...register('email')} />
</div>
<div>
<label>Password</label>
<input type="password" name="password" {...register('password')} />
</div>
<button>Submit</button>
</form>
);
};
export default RegisterForm;
As you can see, no other components were imported to track the input values. The useForm Hook makes the component code cleaner and easier to maintain, and because the form is uncontrolled, you do not have to pass props like onChange and value to each input.
You can use any other UI library of your choice to create the form. But first, make sure to check the documentation, and find the prop used for accessing the reference attribute of the native input component.
In the next section, you will learn how to handle form validation in the form you just built.
To apply validations to a field, you can pass validation parameters to the register method. Validation parameters are similar to the existing HTML form validation standard.
These validation parameters include the following properties:
required indicates if the field is required or not. If this property is set to true, then the field cannot be emptyminlength and maxlength set the minimum and maximum length for a string input valuemin and max set the minimum and maximum values for a numerical valuetype indicates the type of the input field; it can be email, number, text, or any other standard HTML input typespattern defines a pattern for the input value using a regular expressionIf you want to mark a field as required, your code should turn out like this:
<input name="name" type="text" {...register('name', { required: true } )} />
Now try submitting the form with this field empty. This will result in the following error object:
{
name: {
type: "required",
message: "",
ref: <input name="name" type="text" />
}
}
Here, the type property refers to the type of validation that failed, and the ref property contains the native DOM input element.
You can also include a custom error message for the field by passing a string instead of a Boolean to the validation property:
// ...
<form onSubmit={handleSubmit(handleRegistration, handleError)}>
<div>
<label>Name</label>
<input name="name" {...register('name', { required: "Name is required" } )} />
</div>
</form>
Then, access the errors object by using the useForm Hook:
const { register, handleSubmit, formState: { errors } } = useForm();
You can display errors to your users like so:
const RegisterForm = () => {
const { register, handleSubmit, formState: { errors } } = useForm();
const handleRegistration = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(handleRegistration)}>
<div>
<label>Name</label>
<input type="text" name="name" {...register('name')} />
{errors?.name && errors.name.message}
</div>
{/* more input fields... */}
<button>Submit</button>
</form>
);
};
Below you can find the complete example:
import React from "react";
import { useForm } from "react-hook-form";
const RegisterForm = () => {
const { register, handleSubmit, formState: { errors } } = useForm();
const handleRegistration = (data) => console.log(data);
const handleError = (errors) => {};
const registerOptions = {
name: { required: "Name is required" },
email: { required: "Email is required" },
password: {
required: "Password is required",
minLength: {
value: 8,
message: "Password must have at least 8 characters"
}
}
};
return (
<form onSubmit={handleSubmit(handleRegistration, handleError)}>
<div>
<label>Name</label>
<input name="name" type="text" {...register('name', registerOptions.name) }/>
<small className="text-danger">
{errors?.name && errors.name.message}
</small>
</div>
<div>
<label>Email</label>
<input
type="email"
name="email"
{...register('email', registerOptions.email)}
/>
<small className="text-danger">
{errors?.email && errors.email.message}
</small>
</div>
<div>
<label>Password</label>
<input
type="password"
name="password"
{...register('password', registerOptions.password)}
/>
<small className="text-danger">
{errors?.password && errors.password.message}
</small>
</div>
<button>Submit</button>
</form>
);
};
export default RegisterForm;
If you want to validate the field when there is an onChange or onBlur event, you can pass a mode property to the useForm Hook:
const { register, handleSubmit, errors } = useForm({
mode: "onBlur"
});
Find more details on the useForm Hook in the API reference.
In some cases, the external UI component you want to use in your form may not support ref, and can only be controlled by the state.
React Hook Form has provisions for such cases, and can easily integrate with any third-party-controlled components using a Controller component.
React Hook Form provides the wrapper Controller component that allows you to register a controlled external component, similar to how the register method works. In this case, instead of the register method, you will use the control object from the useForm Hook:
const { register, handleSubmit, control } = useForm();
Say that you have to create a role field in your form that will accept values from a select input. You can create the select input using the react-select library.
The control object should be passed to the control prop of the Controller component, along with the name of the field. You can specify the validation rules using the rules prop.
The controlled component should be passed to the Controller component using the as prop. The Select component also requires an options prop to render the dropdown options:
<Controller
name="role"
control={control}
defaultValue=""
rules={registerOptions.role}
render={({ field }) => (
<Select options={selectOptions} {...field} label="Text field" />
)}
/>
The render prop above provides onChange, onBlur, name, ref, and value to the child component. By spreading field into the Select component, React Hook Form registers the input field.
You can check out the complete example for the role field below:
import { useForm, Controller } from "react-hook-form";
import Select from "react-select";
// ...
const { register, handleSubmit, errors, control } = useForm({
// use mode to specify the event that triggers each input field
mode: "onBlur"
});
const selectOptions = [
{ value: "student", label: "Student" },
{ value: "developer", label: "Developer" },
{ value: "manager", label: "Manager" }
];
const registerOptions = {
// ...
role: { required: "Role is required" }
};
// ...
<form>
<div>
<label>Your Role</label>
<Controller
name="role"
control={control}
defaultValue=""
rules={registerOptions.role}
render={({ field }) => (
<Select options={selectOptions} {...field} label="Text field" />
)}
/>
<small className="text-danger">
{errors?.role && errors.role.message}
</small>
</div>
</form>
You can also go through the API reference for the Controller component for a detailed explanation.
useFormContext in React Hook FormuseFormContext is a hook provided by React Hook Form that allows you to access and manipulate the form context/state of deeply nested components. It allows you to share form methods like register, errors, control, etc., within a component without passing props down through multiple levels.
useFormContext is useful when you need to access form methods in deeply nested components or when using custom hooks that need to interact with the form state. Here is how to use useFormContext:
import React from 'react';
import { useForm, FormProvider, useFormContext } from 'react-hook-form';
const Input = ({ name }) => {
const { register } = useFormContext();
return <input {...register(name)} />;
};
const ContextForm = () => {
const methods = useForm();
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(data => console.log(data))}>
<Input name="firstName" />
<Input name="lastName" />
<button type="submit">Submit</button>
</form>
</FormProvider>
);
};
export default ContextForm;
In the example above, Input component uses the useFormContext Hook to access the form method register, allowing it to register the input field without prop drilling from the parent component.
You can also create a component to make it easier for developers to handle more complex forms, such as when inputs are deeply nested within component trees:
import { FormProvider, useForm, useFormContext } from "react-hook-form";
export const ConnectForm = ({ children }) => {
const methods = useFormContext();
return children({ ...methods });
};
export const DeepNest = () => (
<ConnectForm>
{({ register }) => <input {...register("hobbies")} />}
</ConnectForm>
);
export const App = () => {
const methods = useForm();
return (
<FormProvider {...methods}>
<form>
<DeepNest />
</form>
</FormProvider>
);
};
React Hook Form supports arrays and nested fields out of the box, allowing you to easily handle complex data structures.
To work with arrays, you can use the useFieldArray Hook. This is a custom hook provided by React Hook Form that helps with handling form fields, such as arrays of inputs. The hook provides methods to add, remove, and swap array items. Let’s see the useFieldArray Hook in action:
import React from 'react';
import { useForm, FormProvider, useFieldArray, useFormContext } from 'react-hook-form';
const Hobbies = () => {
const { control, register } = useFormContext();
const { fields, append, remove } = useFieldArray({
control,
name: 'hobbies'
});
return (
<div>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`hobbies.${index}.name`)} />
<button type="button" onClick={() => remove(index)}>Remove</button>
</div>
))}
<button type="button" onClick={() => append({ name: '' })}>Add Hobby</button>
</div>
);
};
const MyForm = () => {
const methods = useForm();
const onSubmit = data => {
console.log(data);
};
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Hobbies />
<button type="submit">Submit</button>
</form>
</FormProvider>
);
};
export default MyForm;
From the above code, the Hobbies component uses useFieldArray to manage an array of hobbies. Users can add or remove hobbies dynamically and each hobby has its own set of fields.
You can also opt to control the entire field array to update the field object with each onChange event. You can map the watched field array values to the controlled fields to make sure that input changes reflect on the field object:
import React from 'react';
import { useForm, FormProvider, useFieldArray, useFormContext } from 'react-hook-form';
const Hobbies = () => {
const { control, register, watch } = useFormContext();
const { fields, append, remove } = useFieldArray({
control,
name: 'hobbies'
});
const watchedHobbies = watch("hobbies");
const controlledFields = fields.map((field, index) => ({
...field,
...watchedHobbies[index]
}));
return (
<div>
{controlledFields.map((field, index) => (
<div key={field.id}>
<input {...register(`hobbies.${index}.name`)} defaultValue={field.name} />
<button type="button" onClick={() => remove(index)}>Remove</button>
</div>
))}
<button type="button" onClick={() => append({ name: '' })}>Add Hobby</button>
</div>
);
};
const MyForm = () => {
const methods = useForm({
defaultValues: {
hobbies: [{ name: "Reading" }]
}
});
const onSubmit = data => {
console.log(data);
};
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Hobbies />
<button type="submit">Submit</button>
</form>
</FormProvider>
);
};
export default MyForm;
The code above uses the watch function to monitor changes to the hobbies field array and controlledFields to make sure that each input reflects its latest state.
Nested fields can be handled similarly to arrays. You just need to specify the correct path using dot notation when registering inputs:
import React from 'react';
import { useForm, FormProvider, useFormContext } from 'react-hook-form';
const Address = () => {
const { register } = useFormContext();
return (
<div>
<input {...register('address.street')} placeholder="Street" />
<input {...register('address.city')} placeholder="City" />
</div>
);
};
const MyForm = () => {
const methods = useForm();
const onSubmit = data => {
console.log(data);
};
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Address />
<button type="submit">Submit</button>
</form>
</FormProvider>
);
};
export default MyForm;
In the code above, the Address component registers fields for street and city under the address object in the form state. This way, the form data will be structured as an object with nested properties:
{
"address": {
"street": "value",
"city": "value"
}
}
Using useFormContext with a deeply nested field can affect the performance of your application when it is not managed properly because the FormProvider triggers a re-render whenever the form state updates. Using a tool like React memo can help optimize performance when using the useFormcontext Hook by preventing unnecessary re-renders.
React Hook Form supports validation for arrays and nested fields using the Yup or Zod validation libraries.
The following example sets up validation for the hobbies array and the address object using Yup schema validation. Each hobby name and address field is validated according to the specified rules:
import React from 'react';
import { useForm, FormProvider, useFieldArray, useFormContext } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
const schema = yup.object().shape({
hobbies: yup.array().of(
yup.object().shape({
name: yup.string().required('Hobby is required')
})
),
address: yup.object().shape({
street: yup.string().required('Street is required'),
city: yup.string().required('City is required')
})
});
const Hobbies = () => {
const { control, register } = useFormContext();
const { fields, append, remove } = useFieldArray({
control,
name: 'hobbies'
});
return (
<div>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`hobbies.${index}.name`)} placeholder="Hobby" />
<button type="button" onClick={() => remove(index)}>Remove</button>
</div>
))}
<button type="button" onClick={() => append({ name: 'playing football' })}>Add Hobby</button>
</div>
);
};
const Address = () => {
const { register } = useFormContext();
return (
<div>
<input {...register('address.street')} placeholder="Street" />
<input {...register('address.city')} placeholder="City" />
</div>
);
};
const App = () => {
const methods = useForm({
resolver: yupResolver(schema)
});
const onSubmit = data => {
console.log(data);
};
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Hobbies />
<Address />
<button type="submit">Submit</button>
</form>
</FormProvider>
);
};
export default App;
React Hook Form is an excellent addition to the React open source ecosystem, significantly simplifying the creation and maintenance of forms. Its greatest strengths include its focus on the developer experience and its flexibility. It integrates seamlessly with state management libraries and works excellently in React Native.
You can check out the full code and demo for further reference. Until next time, stay safe and keep building more forms. Cheers ✌
Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not
server-side
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
// Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now
From basic syntax and advanced techniques to practical applications and error handling, here’s how to use node-cron.

The Angular tree view can be hard to get right, but once you understand it, it can be quite a powerful visual representation.

Build a fast, real-time app with Relay 17 to leverage features like optimistic UI updates, GraphQL subscriptions, and seamless data syncing.

Simplify component interaction and dynamic theming in Vue 3 with defineExpose and for better control and flexibility.
3 Replies to "React Hook Form: A guide with examples"
this post is helpful thank you for your work. But it needs updating because of the syntax updates in v7 of RHF especially with the controller. have a nice day
Hi Layla, you can check out this guide for v7 https://blog.logrocket.com/whats-new-in-react-hook-form-v7/
Hey, brilliant, but it needs expansion, like FormProvider etc. or I’d suggest you to remove the “Complete” from the title 🙂