Как я использую React Hook Form

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Приветствую, уважаемые читатели! Сегодня я хочу поделиться своим опытом использования одной из самых популярных библиотек для создания форм на 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.

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

Источник: https://habr.com/ru/articles/746806/


Интересные статьи

Интересные статьи

Привет, друзья! Представляю вашему вниманию перевод этой статьи, вызывавшей определенный резонанс в сообществе React-разработчиков. Дорогой React, мы встречаемся уже почти 10 лет. Мы прошли долг...
Свежие новости и статьи из мира фронтенд-разработки за последнюю неделю 25 апреля–1 мая.— Сделайте так, чтобы поиск по странице работал даже в сворачиваемых элементах, с помощью атрибута hidden, но с ...
Стилизация. Довольно насущный для меня момент. Несмотря на годы работы с React, стартуя новый проект, я каждый раз задумываюсь о стилизации. Я перепробовал многие её способы, больше и дольше всего я...
Привет, друзья! В этой статье я хочу поделиться с вами опытом разработки хука для загрузки дополнительных данных (авось кому-нибудь пригодится). На самом деле, хуков будет целых 2 штуки: useL...
Чем функциональные компоненты React отличаются от компонентов, основанных на классах? Уже довольно давно традиционный ответ на этот вопрос звучит так: «Применение классов позволяет пользоваться б...