Managing Forms with React Hook Form
Updated on · 9 min read|
Dealing with forms in React can be a challenging task, especially when incorporating dynamic fields. Thankfully, several libraries are available to make this process more manageable. One such library is React Hook Form. Rather than relying on predefined form components, React Hook Form leverages hooks to give developers control over the form's behavior while allowing them to define their own component implementations. This flexibility means that users are not confined to any specific UI framework or set of form components.
In this tutorial, we will go through the process of creating a simple recipe form using React Hook Form. This form will include basic details, as well as a dynamic list of ingredients. By the end of this post, you will have a functional form that looks like this:

The final version of the code is available on GitHub and CodeSandbox.
This post covers the fundamentals of working with React Hook Form. If you're curious about more advanced use cases, you may find these posts helpful:
-
Working with multistep forms: Build a Multistep Form With React Hook Form.
-
Saving form data during step navigation and displaying the state of each step in the Stepper component: Advanced Multistep Forms with React Hook Form.
-
Displaying a warning if a user attempts to navigate away from the form while having unsaved data in the form: Display Warning for Unsaved Form Data on Page Exit.
Setup
Apart from React Hook Form, we'll be using @emotion/styled library to add a bit of styling to the form components.
We begin by installing all the required dependencies.
bashnpm i @emotion/core @emotion/styled react-hook-form
bashnpm i @emotion/core @emotion/styled react-hook-form
Now we can set up our form component in a new file, called RecipeForm.js.
jsx// RecipeForm.js import styled from "@emotion/styled"; export const Recipe = () => { return ( <Container> <h1>New recipe</h1> </Container> ); }; const Container = styled.div` display: flex; flex-direction: column; max-width: 700px; `;
jsx// RecipeForm.js import styled from "@emotion/styled"; export const Recipe = () => { return ( <Container> <h1>New recipe</h1> </Container> ); }; const Container = styled.div` display: flex; flex-direction: column; max-width: 700px; `;
Form Basics
With all this setup out of the way, we can finally start working on the form itself. We'll begin with the Basics section, which will have general information about the recipe. To help with grouping form fields into sections, let's add a custom component, called FieldSet
, which is a small abstraction on top of the native HTML fieldset
.
jsx// FieldSet.js import styled from "@emotion/styled"; export const FieldSet = ({ label, children }) => { return ( <Container> {label && <Legend>{label}</Legend>} <Wrapper>{children}</Wrapper> </Container> ); }; const Container = styled.fieldset` margin: 16px 0; padding: 0; border: none; `; const Wrapper = styled.div` display: flex; justify-content: space-between; flex-direction: column; align-items: self-start; `; const Legend = styled.legend` font-size: 16px; font-weight: bold; margin-bottom: 10px; `;
jsx// FieldSet.js import styled from "@emotion/styled"; export const FieldSet = ({ label, children }) => { return ( <Container> {label && <Legend>{label}</Legend>} <Wrapper>{children}</Wrapper> </Container> ); }; const Container = styled.fieldset` margin: 16px 0; padding: 0; border: none; `; const Wrapper = styled.div` display: flex; justify-content: space-between; flex-direction: column; align-items: self-start; `; const Legend = styled.legend` font-size: 16px; font-weight: bold; margin-bottom: 10px; `;
Additionally, we will streamline the form field logic by extracting it into a separate component called Field
. This component will be responsible for rendering input elements and their corresponding labels, as well as managing field error logic, making our code more readable and maintainable.
jsx// Field.js import React from "react"; import styled from "@emotion/styled"; export const Field = ({ label, children, htmlFor, error }) => { const id = htmlFor || getChildId(children); return ( <Container errorState={!!error}> {label && <Label htmlFor={id}>{label}</Label>} {children} {!!error && <ErrorMessage role="alert">{error.message}</ErrorMessage>} </Container> ); }; const getChildId = (children) => { const child = React.Children.only(children); if ("id" in child?.props) { return child.props.id; } }; const Container = styled.div` display: flex; flex-direction: column; align-content: flex-start; justify-content: flex-start; margin: 16px 0; padding: 0; border: none; width: 100%; input, textarea { border-color: ${({ errorState }) => (errorState ? "red" : "#d9d9d9")}; } `; const Label = styled.label` margin-bottom: 2px; `; const ErrorMessage = styled.div` color: red; font-size: 14px; `;
jsx// Field.js import React from "react"; import styled from "@emotion/styled"; export const Field = ({ label, children, htmlFor, error }) => { const id = htmlFor || getChildId(children); return ( <Container errorState={!!error}> {label && <Label htmlFor={id}>{label}</Label>} {children} {!!error && <ErrorMessage role="alert">{error.message}</ErrorMessage>} </Container> ); }; const getChildId = (children) => { const child = React.Children.only(children); if ("id" in child?.props) { return child.props.id; } }; const Container = styled.div` display: flex; flex-direction: column; align-content: flex-start; justify-content: flex-start; margin: 16px 0; padding: 0; border: none; width: 100%; input, textarea { border-color: ${({ errorState }) => (errorState ? "red" : "#d9d9d9")}; } `; const Label = styled.label` margin-bottom: 2px; `; const ErrorMessage = styled.div` color: red; font-size: 14px; `;
Take note that we've included a handy utility function, getChildId
. This function automatically assigns the htmlFor
attribute to the label
based on the id
attribute of the child element. By doing so, it effectively links the label and input elements, enhancing the form's accessibility. In case we need to specify a custom htmlFor
attribute we add an htmlFor
prop.
Now it's time to create the form. For this simple recipe form, we'll only have a few basic fields, such as recipe name, description, and number of servings.
jsximport styled from "@emotion/styled"; import { FieldSet } from "./FieldSet.js"; import { Field } from "./Field.js"; export const RecipeForm = () => { return ( <Container> <h1>New recipe</h1> <form> <FieldSet label="Basics"> <Field label="Name"> <Input type="text" name="name" id="name" /> </Field> <Field label="Description"> <TextArea name="description" id="description" rows={10} /> </Field> <Field label="Servings"> <Input type="number" name="amount" id="amount" /> </Field> </FieldSet> <Field> <Button variant="primary">Save</Button> </Field> </form> </Container> ); }; const Container = styled.div` display: flex; flex-direction: column; max-width: 700px; `; const Input = styled.input` box-sizing: border-box; padding: 10px; width: 100%; border: 1px solid #d9d9d9; border-radius: 6px; `; const TextArea = styled.textarea` box-sizing: border-box; padding: 4px 11px; width: 100%; border: 1px solid #d9d9d9; border-radius: 6px; `; const Button = styled.button` font-size: 14px; cursor: pointer; padding: 0.6em 1.2em; border: 1px solid #d9d9d9; border-radius: 6px; margin-right: auto; background-color: ${({ variant }) => variant === "primary" ? "#3b82f6" : "white"}; color: ${({ variant }) => (variant === "primary" ? "white" : "#213547")}; `;
jsximport styled from "@emotion/styled"; import { FieldSet } from "./FieldSet.js"; import { Field } from "./Field.js"; export const RecipeForm = () => { return ( <Container> <h1>New recipe</h1> <form> <FieldSet label="Basics"> <Field label="Name"> <Input type="text" name="name" id="name" /> </Field> <Field label="Description"> <TextArea name="description" id="description" rows={10} /> </Field> <Field label="Servings"> <Input type="number" name="amount" id="amount" /> </Field> </FieldSet> <Field> <Button variant="primary">Save</Button> </Field> </form> </Container> ); }; const Container = styled.div` display: flex; flex-direction: column; max-width: 700px; `; const Input = styled.input` box-sizing: border-box; padding: 10px; width: 100%; border: 1px solid #d9d9d9; border-radius: 6px; `; const TextArea = styled.textarea` box-sizing: border-box; padding: 4px 11px; width: 100%; border: 1px solid #d9d9d9; border-radius: 6px; `; const Button = styled.button` font-size: 14px; cursor: pointer; padding: 0.6em 1.2em; border: 1px solid #d9d9d9; border-radius: 6px; margin-right: auto; background-color: ${({ variant }) => variant === "primary" ? "#3b82f6" : "white"}; color: ${({ variant }) => (variant === "primary" ? "white" : "#213547")}; `;
In this step, we add the recipe fields along with their labels, resulting in the simple form shown below. Note the use of the name
attributes on the form elements, as they will prove useful shortly. For the automatic htmlFor
assignment to work, we need to remember to add id
to all the input elements.

Manging the form with React Hook Form
Now, let's leverage React Hook Form to manage our form's state. One of the key benefits of this library is that it simplifies state management, eliminating the need for multiple useState
hooks. All we have to do is use the register
function with the field's name as an argument to register fields within the form's state.
jsximport styled from "@emotion/styled"; import { FieldSet } from "./FieldSet.js"; import { Field } from "./Field.js"; import { useForm } from "react-hook-form"; export const RecipeForm = () => { const { register, handleSubmit } = useForm(); const submitForm = (formData) => { console.log(formData); }; return ( <Container> <h1>New recipe</h1> <form onSubmit={handleSubmit(submitForm)}> <FieldSet label="Basics"> <Field label="Name"> <Input {...register("name")} type="text" id="name" /> </Field> <Field label="Description"> <TextArea {...register("description")} id="description" rows={10} /> </Field> <Field label="Servings"> <Input {...register("amount")} type="number" id="amount" /> </Field> </FieldSet> <Field> <Button variant="primary">Save</Button> </Field> </form> </Container> ); };
jsximport styled from "@emotion/styled"; import { FieldSet } from "./FieldSet.js"; import { Field } from "./Field.js"; import { useForm } from "react-hook-form"; export const RecipeForm = () => { const { register, handleSubmit } = useForm(); const submitForm = (formData) => { console.log(formData); }; return ( <Container> <h1>New recipe</h1> <form onSubmit={handleSubmit(submitForm)}> <FieldSet label="Basics"> <Field label="Name"> <Input {...register("name")} type="text" id="name" /> </Field> <Field label="Description"> <TextArea {...register("description")} id="description" rows={10} /> </Field> <Field label="Servings"> <Input {...register("amount")} type="number" id="amount" /> </Field> </FieldSet> <Field> <Button variant="primary">Save</Button> </Field> </form> </Container> ); };
We begin by importing and invoking the useForm
hook, which returns several helpful utilities. In this instance, we use register
to associate a form field with its corresponding property in the state via the field's name. This is why adding names to the fields is crucial here. Additionally, we need to wrap our submit function in the handleSubmit
callback.
Now, if we enter recipe details in the form fields and press Save
, we should see the following object displayed in the console:
json{ name: "Pancakes", description: "Super delicious pancake recipe", amount: "10" }
json{ name: "Pancakes", description: "Super delicious pancake recipe", amount: "10" }
That's all the setup needed to start using React Hook Form. However, its functionality doesn't end here and next, we'll see a few enhancements we can add to our form.
Form validation and error handling
The register value returned from useForm
is a function that accepts validation rules as an object for its second parameter. Several validation rules are available:
- required
- min
- max
- minLength
- maxLength
- pattern
- validate
To make the recipe name a required field, all we need to do is call register
with a required
prop:
jsx<Input {...register("name", { required: true })} type="text" id="name" />
jsx<Input {...register("name", { required: true })} type="text" id="name" />
Additionally, the useForm
function returns a formState
object that contains an errors
attribute. This attribute maps all the raised errors to their corresponding field names. For instance, in the case of a missing recipe name, the errors
object would have a name
attribute with a type of required
.
It's worth noting that instead of specifying the required
validation rule with a boolean value, we can also pass it a string, which will be used as the error message.
jsx<Input {...register("name", { required: 'This field is required' })} type="text" id="name" />
jsx<Input {...register("name", { required: 'This field is required' })} type="text" id="name" />
Alternatively, we can use the message
property to specify an error message. This message can be accessed later via errors.name.message
. We can then pass this error object to the Field
component to toggle the error state.
By combining form validation and errors, we can display helpful messages for the users.
jsximport styled from "@emotion/styled"; import { FieldSet } from "./FieldSet.js"; import { Field } from "./Field.js"; import { useForm } from "react-hook-form"; export const RecipeForm = () => { const { register, handleSubmit, formState: { errors }, } = useForm(); const submitForm = (formData) => { console.log(formData); }; return ( <Container> <h1>New recipe</h1> <form onSubmit={handleSubmit(submitForm)}> <FieldSet label="Basics"> <Field label="Name" error={errors.name}> <Input {...register("name", { required: "Recipe name is required" })} type="text" id="name" /> </Field> <Field label="Description" error={errors.description}> <TextArea {...register("description", { maxLength: { value: 100, message: "Description cannot be longer than 100 characters", }, })} id="description" rows={10} /> </Field> <Field label="Servings" error={errors.amount}> <Input {...register("amount", { max: { value: 10, message: "Maximum number of servings is 10", }, })} type="number" id="amount" /> </Field> </FieldSet> <Field> <Button variant="primary">Save</Button> </Field> </form> </Container> ); };
jsximport styled from "@emotion/styled"; import { FieldSet } from "./FieldSet.js"; import { Field } from "./Field.js"; import { useForm } from "react-hook-form"; export const RecipeForm = () => { const { register, handleSubmit, formState: { errors }, } = useForm(); const submitForm = (formData) => { console.log(formData); }; return ( <Container> <h1>New recipe</h1> <form onSubmit={handleSubmit(submitForm)}> <FieldSet label="Basics"> <Field label="Name" error={errors.name}> <Input {...register("name", { required: "Recipe name is required" })} type="text" id="name" /> </Field> <Field label="Description" error={errors.description}> <TextArea {...register("description", { maxLength: { value: 100, message: "Description cannot be longer than 100 characters", }, })} id="description" rows={10} /> </Field> <Field label="Servings" error={errors.amount}> <Input {...register("amount", { max: { value: 10, message: "Maximum number of servings is 10", }, })} type="number" id="amount" /> </Field> </FieldSet> <Field> <Button variant="primary">Save</Button> </Field> </form> </Container> ); };
If we try to submit the form with invalid data, we get handy validation messages for the corresponding fields.

It's also possible to apply custom validation rules to the fields using the validate
rule. This can be done by defining a function or an object of functions with different validation rules. For instance, we can validate whether the field value is equal using the following code:
jsx<Input {...register("amount", { validate: (value) => value % 2 === 0 })} type="number" />
jsx<Input {...register("amount", { validate: (value) => value % 2 === 0 })} type="number" />
Handling Number Inputs
In the current form, we're using a number input field for the servings. However, due to how HTML input elements work, when the form is submitted, this value will be a string in the form data. In some cases, this might not be what we want, for example, if the data is expected to be a number on the backend. One easy fix would be to convert the amount to a number on submit, but it is not optimal, especially when we have many such fields. A better solution would be to abstract the number input into a separate component with type conversion logic. That way, when the form is submitted, the data has the types we need.
Let's create such a component called NumberInput
.
jsx// NumberInput.js import styled from "@emotion/styled"; export const NumberInput = ({ value, onChange, ...rest }) => { const handleChange = (e) => { const value = e.target.valueAsNumber || 0; onChange(value); }; return ( <Input type="number" min={0} onChange={handleChange} value={value} {...rest} /> ); }; const Input = styled.input` padding: 10px; width: 100%; border: 1px solid #d9d9d9; border-radius: 6px; `;
jsx// NumberInput.js import styled from "@emotion/styled"; export const NumberInput = ({ value, onChange, ...rest }) => { const handleChange = (e) => { const value = e.target.valueAsNumber || 0; onChange(value); }; return ( <Input type="number" min={0} onChange={handleChange} value={value} {...rest} /> ); }; const Input = styled.input` padding: 10px; width: 100%; border: 1px solid #d9d9d9; border-radius: 6px; `;
Here, we use the valueAsNumber property of the event target object to get the numerical representation of the input value. The valueAsNumber
property returns NaN
if the conversion of the input value to a number is impossible. Therefore, we provide 0
as a default value for such cases.
After that, we can replace the current amount
field with this new component.
jsximport styled from "@emotion/styled"; import { FieldSet } from "./FieldSet.js"; import { Field } from "./Field.js"; import { useForm, Controller } from "react-hook-form"; import { NumberInput } from "./NumberInput.js"; export const RecipeForm = () => { const { register, handleSubmit, formState: { errors }, control, } = useForm(); const submitForm = (formData) => { console.log(formData); }; return ( <Container> <h1>New recipe</h1> <form onSubmit={handleSubmit(submitForm)}> <FieldSet label="Basics"> <Field label="Name" error={errors.name}> <Input {...register("name", { required: "Recipe name is required" })} type="text" id="name" /> </Field> <Field label="Description" error={errors.description}> <TextArea {...register("description", { maxLength: { value: 100, message: "Description cannot be longer than 100 characters", }, })} id="description" rows={10} /> </Field> <Field label="Servings" error={errors.amount} htmlFor='amount'> <Controller name="amount" control={control} defaultValue={1} render={({ field: { ref, ...field } }) => ( <NumberInput {...field} id="amount" /> )} rules={{ max: { value: 10, message: "Maximum number of servings is 10", }, }} /> </Field> </FieldSet> <Field> <Button variant="primary">Save</Button> </Field> </form> </Container> ); };
jsximport styled from "@emotion/styled"; import { FieldSet } from "./FieldSet.js"; import { Field } from "./Field.js"; import { useForm, Controller } from "react-hook-form"; import { NumberInput } from "./NumberInput.js"; export const RecipeForm = () => { const { register, handleSubmit, formState: { errors }, control, } = useForm(); const submitForm = (formData) => { console.log(formData); }; return ( <Container> <h1>New recipe</h1> <form onSubmit={handleSubmit(submitForm)}> <FieldSet label="Basics"> <Field label="Name" error={errors.name}> <Input {...register("name", { required: "Recipe name is required" })} type="text" id="name" /> </Field> <Field label="Description" error={errors.description}> <TextArea {...register("description", { maxLength: { value: 100, message: "Description cannot be longer than 100 characters", }, })} id="description" rows={10} /> </Field> <Field label="Servings" error={errors.amount} htmlFor='amount'> <Controller name="amount" control={control} defaultValue={1} render={({ field: { ref, ...field } }) => ( <NumberInput {...field} id="amount" /> )} rules={{ max: { value: 10, message: "Maximum number of servings is 10", }, }} /> </Field> </FieldSet> <Field> <Button variant="primary">Save</Button> </Field> </form> </Container> ); };
To connect this component to the form, React Hook Form provides a Controller
- a wrapper for working with controlled external components.
Instead of using the register
function, we can use the control
object that we get from useForm
. For validation, we use the rules
prop. We still need to add the name
attribute to the Controller
to register it. Then, we pass the input component via a render
prop. This way, the data for the recipe servings will be saved to the form as before, while using an external component.
Controlled vs uncontrolled inputs in React Hook Form
In React Hook Form, the difference between controlled and uncontrolled components is similar to traditional React components. Uncontrolled components in React Hook Form refer to inputs that are registered using the register
function and have their state managed by the DOM. Controlled components, on the other hand, use the useForm
hook to register inputs and manage their state by being wrapped into the Controller
component.
One advantage of using controlled components in React Hook Form is that they provide more granular control over the input state, allowing for easier integration with external libraries and more complex validation logic. Uncontrolled components can be simpler to implement, especially for simple forms, but can be more difficult to customize for more complex use cases.
Handling dynamic fields with useFieldArray
No recipe is complete without its ingredients. However, we can't add fixed ingredient fields to our form since the number of ingredients varies depending on the recipe. Normally, we would need to create our custom logic for handling dynamic fields. However, React Hook Form comes with a custom hook for working with dynamic inputs called useFieldArray
. This hook takes the form's control object and the name for the field, returning several utilities for working with dynamic inputs. Let's see it in action by adding the ingredients fields to our recipe form.
jsximport styled from "@emotion/styled"; import { FieldSet } from "./FieldSet.js"; import { Field } from "./Field.js"; import { useForm, Controller, useFieldArray } from "react-hook-form"; import { NumberInput } from "./NumberInput.js"; export const RecipeForm = () => { const { register, handleSubmit, formState: { errors }, control, } = useForm(); const { fields, append, remove } = useFieldArray({ name: "ingredients", control, }); const submitForm = (formData) => { console.log(formData); }; return ( <Container> <h1>New recipe</h1> <form onSubmit={handleSubmit(submitForm)}> <FieldSet label="Basics"> <Field label="Name" error={errors.name}> <Input {...register("name", { required: "Recipe name is required" })} type="text" id="name" /> </Field> <Field label="Description" error={errors.description}> <TextArea {...register("description", { maxLength: { value: 100, message: "Description cannot be longer than 100 characters", }, })} id="description" rows={10} /> </Field> <Field label="Servings" error={errors.amount} htmlFor='amount'> <Controller name="amount" control={control} defaultValue={1} render={({ field: { ref, ...field } }) => ( <NumberInput {...field} id="amount" /> )} rules={{ max: { value: 10, message: "Maximum number of servings is 10", }, }} /> </Field> </FieldSet> <FieldSet label="Ingredients"> {fields.map((field, index) => { return ( <Row key={field.id}> <Field label="Name"> <Input type="text" {...register(`ingredients[${index}].name`)} id={`ingredients[${index}].name`} /> </Field> <Field label="Amount"> <Input type="text" {...register(`ingredients[${index}].amount`)} defaultValue={field.amount} id={`ingredients[${index}].amount`} /> </Field> <Button type="button" onClick={() => remove(index)}> − </Button> </Row> ); })} <Button type="button" onClick={() => append({ name: "", amount: "" })} > Add ingredient </Button> </FieldSet> <Field> <Button variant="primary">Save</Button> </Field> </form> </Container> ); }; const Row = styled.div` display: flex; align-items: center; justify-content: space-between; width: 100%; & > * { margin-right: 20px; } button { margin: 25px 0 0 8px; } `;
jsximport styled from "@emotion/styled"; import { FieldSet } from "./FieldSet.js"; import { Field } from "./Field.js"; import { useForm, Controller, useFieldArray } from "react-hook-form"; import { NumberInput } from "./NumberInput.js"; export const RecipeForm = () => { const { register, handleSubmit, formState: { errors }, control, } = useForm(); const { fields, append, remove } = useFieldArray({ name: "ingredients", control, }); const submitForm = (formData) => { console.log(formData); }; return ( <Container> <h1>New recipe</h1> <form onSubmit={handleSubmit(submitForm)}> <FieldSet label="Basics"> <Field label="Name" error={errors.name}> <Input {...register("name", { required: "Recipe name is required" })} type="text" id="name" /> </Field> <Field label="Description" error={errors.description}> <TextArea {...register("description", { maxLength: { value: 100, message: "Description cannot be longer than 100 characters", }, })} id="description" rows={10} /> </Field> <Field label="Servings" error={errors.amount} htmlFor='amount'> <Controller name="amount" control={control} defaultValue={1} render={({ field: { ref, ...field } }) => ( <NumberInput {...field} id="amount" /> )} rules={{ max: { value: 10, message: "Maximum number of servings is 10", }, }} /> </Field> </FieldSet> <FieldSet label="Ingredients"> {fields.map((field, index) => { return ( <Row key={field.id}> <Field label="Name"> <Input type="text" {...register(`ingredients[${index}].name`)} id={`ingredients[${index}].name`} /> </Field> <Field label="Amount"> <Input type="text" {...register(`ingredients[${index}].amount`)} defaultValue={field.amount} id={`ingredients[${index}].amount`} /> </Field> <Button type="button" onClick={() => remove(index)}> − </Button> </Row> ); })} <Button type="button" onClick={() => append({ name: "", amount: "" })} > Add ingredient </Button> </FieldSet> <Field> <Button variant="primary">Save</Button> </Field> </form> </Container> ); }; const Row = styled.div` display: flex; align-items: center; justify-content: space-between; width: 100%; & > * { margin-right: 20px; } button { margin: 25px 0 0 8px; } `;
useFieldArray
returns several utilities for managing dynamic fields, including append
, remove
, and the array of the fields themselves. The complete list of utility functions is available on the library's documentation site. Since we do not have default values for ingredients, the field is initially empty. We can start populating it by using the append
function and providing default values for empty fields. Note that the fields are rendered by their index in the array, so it's important to have field names in the format fieldArrayName[fieldIndex][fieldName]
. We can also delete fields by passing the index of the field to the delete
function. Now, after adding a few ingredient fields and filling in their values, when we submit the form, all those values will be saved in the ingredients
field in the form.
That's all it takes to build a fully functional and easily manageable form with React Hook Form. The library has plenty more features, not covered in this post, so make sure to check the documentation for more examples.
Testing the form
Testing forms is an essential part of the development process as it helps ensure that the form works correctly and can catch any future regressions.
Testing forms is a broad topic on its own, and it is covered in a separate post: Testing React Hook Form With React Testing Library.
Conclusion
In this tutorial, we covered the basics of using React Hook Form, a library for managing forms in React. We created a simple recipe form, including basic fields like recipe name, description, and servings. We then learned how to handle form validation and error messages, as well as how to create custom validation rules. We also demonstrated how to use the Controller
component for working with controlled input fields.
Finally, we explored how to add dynamic fields using the useFieldArray
hook, which allowed us to dynamically add and remove ingredients. By the end of the tutorial, we had a fully functional form that can handle dynamic fields with ease.
Dealing with forms in React can indeed be challenging, especially when it comes to dynamic fields. However, as we've demonstrated in this tutorial, using React Hook Form can significantly simplify the process. With features like built-in validation, error handling, and support for dynamic fields, React Hook Form is a powerful tool for building robust and maintainable forms.