How do I use React Hook Form

Greetings, dear readers! Today I want to share my experience of using one of the most popular libraries for creating React forms – React Hook Form. When I first started using this wonderful library, I made a few mistakes that I hope you can avoid.

Used libraries

  1. React 18.2.0

  2. React Hook Form v7.45.1

  3. Material UI v5.13.7

  4. Axios v1.4.0

  5. JSON server v0.17.3

Create and fill out a form

In this article, we will create a form for adding and editing users. Let’s start with the following code:

import { Button, TextField } from "@mui/material";
import { Controller, FormProvider, useForm } from "react-hook-form";
import "./App.css"

export const App = () => {
  const methods = useForm()

  const { control, handleSubmit } = methods

  const onSave = (data) => {
    console.log(data)
  }

  return (
    <FormProvider {...methods}>
      <div className="card">
        <span>Пользователь</span>
        <Controller
          name="name"
          control={control}
          render={({ field: { value, onChange } }) => (
            <TextField
              value={value}
              onChange={onChange}
            />
          )}
        />
        <Controller
          name="suname"
          control={control}
          render={({ field: { value, onChange } }) => (
            <TextField
              value={value}
              onChange={onChange}
            />
          )}
        />
      </div>
      <Button onClick={handleSubmit(onSave)}>Сохранить</Button>
    </FormProvider>
  );
}

In the above code, we are importing the required components and libraries. We then use the hook useFormto get the methods we need from the React Hook Form. We then use destructuring to get the variable methodswhich we will need later.

We wrap our form in FormProvider and pass all the methods we got from useFormlike props.

To register fields, use the component Controllerprovided by React Hook Form.

However, we can have many users, so it makes sense to move the user card itself into a separate component.

import { TextField } from "@mui/material"
import { Controller, useFormContext } from "react-hook-form"

export const UserCard = () => {
    const { control } = useFormContext()

    return (
        <div className="card">
            <span>Пользователь</span>
            <Controller
                name="name"
                control={control}
                render={({ field: { value, onChange } }) => (
                    <TextField
                      value={value}
                      onChange={onChange}
                    />
                )}
            />
            <Controller
                name="suname"
                control={control}
                render={({ field: { value, onChange } }) => (
                    <TextField
                      value={value}
                      onChange={onChange}
                    />
                )}
            />
      </div>
    )
}

Please note that when using Controller we also need to pass control from our form. But if we call useForm again, we create a new form. To get methods in the context of the same form, you can use the hook useFormContext. It returns the same methods as useFormbut already in the context of our form, due to the fact that the form is wrapped in FormProvider. Thus, being at any level within our form, we can always get all its methods.

Here’s what our form looks like now:

import { Button } from "@mui/material";
import { FormProvider, useForm } from "react-hook-form";
import "./App.css"
import { UserCard } from "./UserCard";

export const App = () => {
  const methods = useForm()

  const { handleSubmit } = methods

  const onSave = (data) => {
    console.log(data)
  }

  return (
    <FormProvider {...methods}>
      <UserCard />
      <Button onClick={handleSubmit(onSave)}>Сохранить</Button>
    </FormProvider>
  );
}

Learning to work with arrays in the form

Since we will have an array of users, the form is not quite correct. At the moment we have only two fields. And our form state should contain an array of user objects with name and surname fields. We will be querying users via the API, for this I will use JSON-server and create some users.

{
    "users": [
        {
            "id": 1,
            "name": "Artem",
            "suname": "Morozov"
        },
        {
            "id": 2,
            "name": "Maxim",
            "suname": "Klever"
        },
        {
            "id": 3,
            "name": "John",
            "suname": "Weelson"
        }
    ]
}

Let’s start modifying our form, get the data and write it to the state.

import { useEffect } from "react"
import { Button } from "@mui/material";
import { FormProvider, useFieldArray, useForm } from "react-hook-form";
import "./App.css"
import { UserCard } from "./UserCard";
import axios from "axios";

export const App = () => {
  const methods = useForm({
    defaultValues: {
      users: []
    }
  })

  const { control, handleSubmit, reset } = methods

  const { fields } = useFieldArray({
    name: "users",
    control: control,
    shouldUnregister: true
  })

  const onSave = (data) => {
    console.log(data)
  }

  useEffect(() => {
    const getUsersAsync = async () => {
      const { data } = await axios.get("http://localhost:3000/users")
      reset({
        users: data
      })
    }
    getUsersAsync()
  }, [reset])

  return (
    <FormProvider {...methods}>
      {fields.map((user, index) => (
        <UserCard key={user.id} user={user} userIndex={index} />
      ))}
      <Button onClick={handleSubmit(onSave)}>Сохранить</Button>
    </FormProvider>
  );
}
  1. In the useForm hook, we specify the default values, we only have an array of users.

  2. We get the data using the useFieldArray ( fields ) hook.

  3. We request data from the API and re-render our form using the reset method.

  4. And, accordingly, we go through the array into which the data from our API was written.

Let’s now look at the card code.

import { TextField } from "@mui/material"
import { Controller, useFormContext } from "react-hook-form"
import "./App.css"

export const UserCard = (props) => {
    const { user: { name, suname }, userIndex } = props
    const { control } = useFormContext()

    return (
        <div className="card">
            <div className="card__header">
                <span>Пользователь {userIndex + 1}</span>
            </div>
            <Controller
                name={`users[${userIndex}].name`}
                control={control}
                defaultValue={name}
                render={({ field: { value, onChange } }) => (
                    <TextField
                        value={value}
                        onChange={onChange}
                    />
                )}
            />
            <Controller
                name={`users[${userIndex}].suname`}
                control={control}
                defaultValue={suname}
                render={({ field: { value, onChange } }) => (
                    <TextField
                        value={value}
                        onChange={onChange}
                    />
                )}
            />
      </div>
    )
}
  1. Note that the Controller is now passed defaultValue with the value from props.

  2. The name for each field has also changed. Since users is an array, we specify the index of the element in square brackets, followed by name and surname. You can go to the console and see what’s going on.

Card List Management

Let’s implement the main task of our form – adding and removing users from the list. To do this, we will use the same useFieldArray, which, in addition to fields, returns enough methods that allow us to implement most scenarios. We only need append and remove. Here is how I implemented it.

import { useEffect } from "react"
import { Button } from "@mui/material";
import { FormProvider, useFieldArray, useForm } from "react-hook-form";
import { UserCard } from "./UserCard";
import axios from "axios";

export const App = () => {
  const methods = useForm({
    defaultValues: {
      users: []
    }
  })

  const { control, handleSubmit, reset } = methods

  const { append, remove, fields } = useFieldArray({
    name: "users",
    control: control
  })

  const onSave = (data) => {
    console.log(data)
  }

  const onAddUser = () => {
    const lastUser = fields.at(-1)
    let newUserId = 1;

    if (lastUser) {
      newUserId = lastUser.id + 1;
    }
    
    append({
      id: newUserId,
      name: "",
      suname: ""
    })
  }

  const onDeleteUser = (userIndex) => {
    remove(userIndex)
  }

  useEffect(() => {
    const getUsersAsync = async () => {
      const { data } = await axios.get("http://localhost:3000/users")
      reset({
        users: data
      })
    }
    getUsersAsync()
  }, [reset])

  return (
    <FormProvider {...methods}>
      {fields?.map((user, index) => (
        <UserCard key={index} user={user} userIndex={index} onDeleteUser={onDeleteUser} />
      ))}
      <Button onClick={onAddUser}>Добавить пользователя</Button>
      <Button onClick={handleSubmit(onSave)}>Сохранить</Button>
    </FormProvider>
  );
}

In the add function, we need to get the new id. To do this, we get the last id and just add one. It is worth noting that in the useFieldArray hook, the keyName field was added with the value key. This is done because by default useFieldArray adds the id field, but since our id comes from the API, and when added it is formed on the client, this key should be named differently to avoid conflicts.

In the remove function, we simply call the remove method, passing in the index of the card to be removed as an argument. The index is passed for each card in props.

And finally, the final version of UserCard.

import { Button, TextField } from "@mui/material"
import { Controller, useFormContext } from "react-hook-form"
import "./App.css"

export const UserCard = (props) => {
    const { user: { name, suname }, userIndex, onDeleteUser } = props
    const { control } = useFormContext()

    return (
        <div className="card">
            <div className="card__header">
                <span>Пользователь {userIndex + 1}</span>
                <Button onClick={() => onDeleteUser(userIndex)}>Удалить пользователя</Button>
            </div>

            <Controller
                name={`users[${userIndex}].name`}
                control={control}
                defaultValue={name}
                render={({ field: { value, onChange } }) => (
                    <TextField
                        value={value}
                        onChange={onChange}
                    />
                )}
            />
            <Controller
                name={`users[${userIndex}].suname`}
                control={control}
                defaultValue={suname}
                render={({ field: { value, onChange } }) => (
                    <TextField
                        value={value}
                        onChange={onChange}
                    />
                )}
            />
      </div>
    )
}

Here we have added a delete button and we are calling the delete function on click.

Thank you very much for reading to the end. I would be very grateful for feedback and pointing out errors. Tell us about your experience with React Hook Form.

PS: This code was written in JavaScript solely to reduce the amount of code and make it easier to read. I also didn’t use useCallback memoization as it would make it harder to read. Thank you again.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *