Приветствую, уважаемые читатели! Сегодня я хочу поделиться своим опытом использования одной из самых популярных библиотек для создания форм на React - React Hook Form. Когда я только начинал использовать эту замечательную библиотеку, я совершил несколько ошибок, которые я надеюсь, вы сможете избежать.

Используемые библиотеки

  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

Создание и заполнение формы

В этой статье мы создадим форму для добавления и редактирования пользователей. Давайте начнем со следующего кода:

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>
  );
}

В приведенном выше коде мы импортируем необходимые компоненты и библиотеки. Затем мы используем хук useForm, чтобы получить нужные нам методы из React Hook Form. Затем мы используем деструктуризацию для получения переменной methods, которая понадобится нам позже.

Мы оборачиваем нашу форму в FormProvider и передаем все методы, которые мы получили из useForm, как пропсы.

Для регистрации полей воспользуемся компонентом Controller, предоставляемым React Hook Form.

Однако у нас может быть много пользователей, поэтому имеет смысл вынести саму карточку пользователя в отдельный компонент.

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>
    )
}

Обратите внимание, что при использовании Controller нам также нужно передать control из нашей формы. Но если мы вызываем useForm снова, мы создаем новую форму. Чтобы получить методы в контексте той же формы, можно использовать хук useFormContext. Он возвращает те же методы, что и useForm, но уже в контексте нашей формы, благодаря тому, что форма обернута в FormProvider. Таким образом, находясь на любом уровне внутри нашей формы, мы всегда можем получить все ее методы.

Вот как теперь выглядит наша форма:

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>
  );
}

Учимся работать с массивами в форме

Поскольку у нас будет массив пользователей, форма не совсем корректна. В данный момент у нас есть всего два поля. А состояние нашей формы должно содержать массив объектов user с полями name и surname. Мы будем запрашивать пользователей через API, для этого я воспользуюсь JSON-server и создам несколько пользователей.

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

Давайте начнем изменять нашу форму, получим данные и запишем их в состояние.

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. В хуке useForm мы указываем значения по умолчанию, у нас только массив пользователей.

  2. Получаем данные, используя хук useFieldArray ( fields ).

  3. Запрашиваем данные с API и перерендериваем нашу форму с помощью метода reset.

  4. И, соответственно, проходим по массиву, в который записались данные с нашего API.

Давайте теперь посмотрим на код карточки.

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. Обратите внимание, что в Controller теперь передается defaultValue со значением из props.

  2. Изменился также name для каждого поля. Поскольку users - это массив, мы указываем индекс элемента в квадратных скобках, а затем name и surname. Вы можете зайти в консоль и посмотреть, что происходит.

Управление списком карточек

Давайте реализуем главную задачу нашей формы - добавление и удаление пользователей из списка. Для этого воспользуемся тем же useFieldArray, который помимо fields возвращает достаточно методов, позволяющих реализовать большинство сценариев. Нам нужны только append и remove. Вот как я это реализовал.

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>
  );
}

В функции добавления нам нужно получить новый id. Для этого мы получаем последний id и просто добавляем единицу. Стоит обратить внимание, что в хуке useFieldArray было добавлено поле keyName со значением key. Это сделано, потому что по умолчанию useFieldArray добавляет поле id, но так как у нас id приходит с API, а при добавлении формируется на клиенте, этот ключ следует назвать иначе, чтобы избежать конфликтов.

В функции удаления мы просто вызываем метод remove, передавая в качестве аргумента индекс карточки, которую нужно удалить. Индекс передается для каждой карточки в props.

И, наконец, конечная версия 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>
    )
}

Здесь мы добавили кнопку удаления и вызываем функцию удаления при клике.

Большое спасибо, что дочитали до конца. Буду очень благодарен за обратную связь и указание на ошибки. Расскажите о своем опыте использования React Hook Form.

P.S.: Данный код был написан на JavaScript исключительно для уменьшения количества кода и упрощения чтения. Я также не стал использовать мемоизацию useCallback, поскольку это усложнило бы читаемость. Еще раз благодарю.

Комментарии (4)


  1. yroman
    08.07.2023 23:23
    +1

    Большая часть статьи - пересказ документации по библиотеке. У библиотеки весьма богатая документация с примерами многих сценариев использования.
    Вы упоминаете об ошибках, которые совершили изначально, но при этом никак их не описываете. В чём ценность статьи, в пересказе доки?


    1. artemmorozov13 Автор
      08.07.2023 23:23

      Привет, да в документации, есть много примеров, но нет какого-то полного решения, где в одной форме использовалось бы сразу несколько хуков, в связке с тем же, FormProvider и рендерингом через Controller. Если использовать документацию, но придется потратить гораздо больше времени, чтобы собрать все вместе, писал со своим восприятием. Мне бы данный пример очень бы помог разобраться как делать динамические формы, в свое время.

      Ну и несмотря на то, что есть документация очень на многих проектах приходилось видеть игнорирование, большинства хуков и практик. Бесконечное использование setValues, watch и ref


  1. TENEr98
    08.07.2023 23:23

    хотелось бы порядок в импортах с помощью.
    eslint-plugin-import

    Используемые библеотеки
    пожалуйста поправь на
    "Используемые библиотеки"


    1. artemmorozov13 Автор
      08.07.2023 23:23

      СПС