React Hook Form
useForm, register, handleSubmit, formState, Controller, useFieldArray, режимы валидации.
Обновлено
Что такое React Hook Form
React Hook Form (RHF) — самая популярная библиотека форм для React. Один хук, useForm, даёт всё нужное: регистрацию полей, валидацию, ошибки, состояние отправки.
Секрет производительности: она построена на неконтролируемых полях. Значения живут в DOM, а не в состоянии React → ввод не перерисовывает всю форму. На 50 полях разница ощутима.
'use client'
import { useForm } from 'react-hook-form'
type Values = { email: string; password: string }
export function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<Values>({ defaultValues: { email: '', password: '' } })
const onSubmit = async (values: Values) => {
await login(values)
}
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<input type="email" {...register('email', { required: 'Email обязателен' })} aria-invalid={!!errors.email} />
{errors.email && <p role="alert">{errors.email.message}</p>}
<input type="password" {...register('password', { minLength: { value: 8, message: 'Минимум 8 символов' } })} />
{errors.password && <p role="alert">{errors.password.message}</p>}
<button disabled={isSubmitting}>Войти</button>
</form>
)
}Части useForm
| Часть | Что делает |
|---|---|
register('name', rules) |
подключает поле: возвращает name, ref, onChange, onBlur |
handleSubmit(fn) |
валидирует, затем вызывает fn(values), только если всё верно |
formState.errors |
ошибки по полям: errors.email?.message |
formState.isSubmitting |
true, пока выполняется onSubmit (async) |
formState.isDirty / dirtyFields |
что изменилось относительно defaultValues |
formState.isValid |
форма сейчас валидна |
watch('name') |
подписка на значение — перерисовка при изменении (используйте умеренно) |
setValue, reset, setError |
управление из кода: заполнить, сбросить, ошибка с сервера |
Когда проверять — режим
useForm({ mode: 'onTouched' })mode |
Проверяет | Ощущение |
|---|---|---|
onSubmit (по умолчанию) |
при отправке, затем при каждом изменении | ошибки только после первой отправки |
onBlur |
при уходе с поля | — |
onTouched |
после первого blur, затем при каждом изменении | самый дружелюбный — не ругается, пока вы печатаете |
onChange |
при каждом нажатии | агрессивно, дорого |
Компоненты, которые не являются нативными полями: Controller
register работает с <input>, <select>, <textarea>. Для своих компонентов (выбор даты, select из UI-библиотеки) используют Controller, который превращает их в контролируемые поля:
<Controller
control={control}
name="country"
render={({ field, fieldState }) => <CountrySelect {...field} error={fieldState.error?.message} />}
/>Динамические списки: useFieldArray
const { fields, append, remove } = useFieldArray({ control, name: 'phones' })
{fields.map((field, i) => (
<div key={field.id}> {/* field.id, а не индекс */}
<input {...register(`phones.${i}.number`)} />
<button type="button" onClick={() => remove(i)}>Удалить</button>
</div>
))}
<button type="button" onClick={() => append({ number: '' })}>Добавить телефон</button>Валидация через схему
Правила, написанные в register, разбросаны по JSX. Рекомендуемый вариант: схема Zod + zodResolver — типы выводятся из схемы, а та же схема валидирует и на сервере. Подробнее — в уроке про Zod.
const form = useForm({ resolver: zodResolver(loginSchema), mode: 'onTouched' })Частые ошибки
watch()без аргумента в главном компоненте → перерисовка всей формы при каждом нажатии. ИспользуйтеuseWatch({ name })в маленьком компоненте.key={index}вuseFieldArray— используйтеfield.id.- Валидация только в RHF, без проверки на сервере.
Коротко
useForm→registerна полях,handleSubmit(onSubmit),formState.errors.- Внутри — неконтролируемые поля → быстро;
Controllerдля своих компонентов,useFieldArrayдля списков. mode: 'onTouched'+ схема Zod черезzodResolver.