Как я использую React Hook Form
- понедельник, 10 июля 2023 г. в 00:00:10
Приветствую, уважаемые читатели! Сегодня я хочу поделиться своим опытом использования одной из самых популярных библиотек для создания форм на 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.
PS: Данный код был написан на JavaScript исключительно для уменьшения количества кода и упрощения чтения. Я также не стал использовать мемоизацию useCallback, поскольку это усложнило бы читаемость. Еще раз благодарю.