ДокументацияReactReact Hook Form — формы и валидация
Средний 12 мин чтения

React Hook Form — формы и валидация

React Hook Form — производительная библиотека для работы с формами. Регистрация полей, валидация, схемы Zod, обработка ошибок и работа с UI-библиотеками.

React Hook FormформывалидацияZodReact

Установка

npm install react-hook-form

Базовая форма

import { useForm } from 'react-hook-form'

interface LoginForm {
  email: string
  password: string
}

function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<LoginForm>()

  function onSubmit(data: LoginForm) {
    console.log(data) // { email: '...', password: '...' }
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>Email</label>
        <input
          type="email"
          {...register('email', {
            required: 'Введите email',
            pattern: {
              value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
              message: 'Некорректный email',
            },
          })}
        />
        {errors.email && <span className="text-red-500">{errors.email.message}</span>}
      </div>

      <div>
        <label>Пароль</label>
        <input
          type="password"
          {...register('password', {
            required: 'Введите пароль',
            minLength: { value: 8, message: 'Минимум 8 символов' },
          })}
        />
        {errors.password && <span className="text-red-500">{errors.password.message}</span>}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Вход...' : 'Войти'}
      </button>
    </form>
  )
}

register связывает input с формой. handleSubmit валидирует данные и вызывает onSubmit только если всё заполнено верно.

Встроенные правила валидации

{...register('username', {
  required: 'Обязательное поле',
  minLength: { value: 3, message: 'Минимум 3 символа' },
  maxLength: { value: 20, message: 'Максимум 20 символов' },
  pattern: {
    value: /^[a-zA-Z0-9_]+$/,
    message: 'Только латиница, цифры и подчёркивание',
  },
  validate: {
    noSpaces: (value) => !value.includes(' ') || 'Без пробелов',
    notAdmin: (value) => value !== 'admin' || 'Имя занято',
  },
})}

Валидация с Zod

Для сложных форм удобнее описывать схему валидации отдельно:

npm install @hookform/resolvers zod
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

const registerSchema = z.object({
  name: z.string().min(2, 'Имя минимум 2 символа'),
  email: z.string().email('Некорректный email'),
  password: z.string().min(8, 'Минимум 8 символов'),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: 'Пароли не совпадают',
  path: ['confirmPassword'],
})

type RegisterForm = z.infer<typeof registerSchema>

function RegisterForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<RegisterForm>({
    resolver: zodResolver(registerSchema),
  })

  function onSubmit(data: RegisterForm) {
    console.log(data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} placeholder="Имя" />
      {errors.name && <span>{errors.name.message}</span>}

      <input {...register('email')} placeholder="Email" />
      {errors.email && <span>{errors.email.message}</span>}

      <input type="password" {...register('password')} placeholder="Пароль" />
      {errors.password && <span>{errors.password.message}</span>}

      <input type="password" {...register('confirmPassword')} placeholder="Повторите пароль" />
      {errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}

      <button type="submit">Зарегистрироваться</button>
    </form>
  )
}

setValue и getValue

function ProfileForm({ userId }: { userId: number }) {
  const { register, setValue, getValues } = useForm<UserForm>()

  useEffect(() => {
    async function loadUser() {
      const res = await fetch(`/api/users/${userId}`)
      const user = await res.json()
      setValue('name', user.name)
      setValue('email', user.email)
      setValue('bio', user.bio)
    }
    loadUser()
  }, [userId, setValue])

  function handleAutoFill() {
    const name = getValues('name')
    if (name) {
      setValue('slug', name.toLowerCase().replace(/\s+/g, '-'))
    }
  }
}

Controller для кастомных компонентов

register работает только с нативными input. Для кастомных компонентов (date picker, select из UI-библиотеки) используйте Controller:

import { Controller } from 'react-hook-form'

function ProductForm() {
  const { control, handleSubmit } = useForm<ProductForm>()

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="category"
        control={control}
        rules={{ required: 'Выберите категорию' }}
        render={({ field, fieldState }) => (
          <CustomSelect
            value={field.value}
            onChange={field.onChange}
            options={[
              { value: 'electronics', label: 'Электроника' },
              { value: 'clothing', label: 'Одежда' },
              { value: 'books', label: 'Книги' },
            ]}
            error={fieldState.error?.message}
          />
        )}
      />
    </form>
  )
}

useFieldArray — динамические поля

Для списков (несколько телефонов, несколько адресов):

import { useForm, useFieldArray } from 'react-hook-form'

interface RecipeForm {
  title: string
  ingredients: { name: string; amount: string }[]
}

function RecipeForm() {
  const { register, control, handleSubmit } = useForm<RecipeForm>({
    defaultValues: {
      title: '',
      ingredients: [{ name: '', amount: '' }],
    },
  })

  const { fields, append, remove } = useFieldArray({
    control,
    name: 'ingredients',
  })

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('title', { required: true })} placeholder="Название" />

      {fields.map((field, index) => (
        <div key={field.id}>
          <input {...register(`ingredients.${index}.name`)} placeholder="Ингредиент" />
          <input {...register(`ingredients.${index}.amount`)} placeholder="Количество" />
          <button type="button" onClick={() => remove(index)}>×</button>
        </div>
      ))}

      <button type="button" onClick={() => append({ name: '', amount: '' })}>
        + Добавить ингредиент
      </button>

      <button type="submit">Сохранить</button>
    </form>
  )
}

Форма с UI-библиотекой (Shadcn/ui)

import { useForm } from 'react-hook-form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'

function SearchForm() {
  const { register, handleSubmit } = useForm<{ query: string }>()

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))} className="flex gap-2">
      <Label htmlFor="query" className="sr-only">Поиск</Label>
      <Input id="query" {...register('query', { required: true })} placeholder="Поиск..." />
      <Button type="submit">Найти</Button>
    </form>
  )
}

Основные методы

МетодНазначение
registerСвязать нативный input с формой
handleSubmitОбработчик submit с валидацией
watchСледить за значениями в реальном времени
setValueПрограммно установить значение
getValuesПолучить текущие значения
resetСбросить форму к начальным значениям
setErrorУстановить ошибку вручную
triggerЗапустить валидацию программно
controlДля использования с Controller

Итог

React Hook Form — стандарт де-факто для форм в React. Минимальные ре-рендеры, гибкая валидация (встроенная или через Zod), удобная работа с динамическими полями. В связке с @hookform/resolvers и Zod получается типобезопасная валидация с минимальным boilerplate.