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>
Would you be interested in joining LogRocket's developer community?
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 nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
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 🙂