Приветствую, уважаемые читатели! Сегодня я хочу поделиться своим опытом использования одной из самых популярных библиотек для создания форм на React - React Hook Form. Когда я только начинал использовать эту замечательную библиотеку, я совершил несколько ошибок, которые я надеюсь, вы сможете избежать.
Используемые библиотеки
React 18.2.0
React Hook Form v7.45.1
Material UI v5.13.7
Axios v1.4.0
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> ); }
В хуке useForm мы указываем значения по умолчанию, у нас только массив пользователей.
Получаем данные, используя хук useFieldArray ( fields ).
Запрашиваем данные с API и перерендериваем нашу форму с помощью метода reset.
И, соответственно, проходим по массиву, в который записались данные с нашего 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> ) }
Обратите внимание, что в Controller теперь передается defaultValue со значением из props.
Изменился также 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, поскольку это усложнило бы читаемость. Еще раз благодарю.
