Валидация форм в SPA

Валидация форм в SPA (Single Page Application) реализуется через специализированные библиотеки управления формами (React Hook Form, Formik, VeeValidate), которые сочетают схемы валидации с реактивным состоянием.

Зачем нужно

В SPA нет нативной отправки формы — данные отправляются через fetch/axios. Библиотеки форм берут на себя: отслеживание состояния полей (touched, dirty, valid), показ ошибок в нужный момент, производительный рендеринг (без re-render всей формы при каждом нажатии клавиши), интеграцию со схемами валидации Zod/Yup. Без библиотеки управление формой из 10+ полей превращается в хаос useState.

Где используется

  • Многошаговые формы регистрации/оформления заказа
  • Формы с динамическими полями (добавить адрес доставки)
  • Формы с межполевой валидацией (пароль = подтверждение пароля)
  • Формы с файлами, rich text, datepicker

React Hook Form + Zod

Наиболее популярный стек для React:

// schema.js
import { z } from 'zod';

export const registerSchema = z.object({
  email: z.string.email('Введите корректный email'),
  password: z.string.min(8, 'Пароль не менее 8 символов'),
  confirmPassword: z.string,
}).refine(
  (data) => data.password === data.confirmPassword,
  {
    message: 'Пароли не совпадают',
    path: ['confirmPassword'],
  }
);
// RegisterForm.jsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { registerSchema } from './schema';

export function RegisterForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm({
    resolver: zodResolver(registerSchema),
    mode: 'onBlur', // валидация при потере фокуса
  });

  const onSubmit = async (data) => {
    await fetch('/api/register', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          {...register('email')}
          aria-describedby={errors.email ? 'email-error' : undefined}
          aria-invalid={!!errors.email}
        />
        {errors.email && (
          <p id="email-error" role="alert">{errors.email.message}</p>
        )}
      </div>

      <div>
        <label htmlFor="password">Пароль</label>
        <input
          id="password"
          type="password"
          {...register('password')}
          aria-invalid={!!errors.password}
        />
        {errors.password && (
          <p role="alert">{errors.password.message}</p>
        )}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Отправка...' : 'Зарегистрироваться'}
      </button>
    </form>
  );
}

Доступность ошибок валидации (a11y)

{/* role="alert" — screen reader немедленно озвучит ошибку */}
{errors.email && (
  <p id="email-error" role="alert" aria-live="polite">
    {errors.email.message}
  </p>
)}

{/* aria-invalid + aria-describedby — связь поля с ошибкой */}
<input
  type="email"
  aria-invalid={!!errors.email}
  aria-describedby="email-error"
/>

Стратегии показа ошибок

Стратегия React Hook Form mode Когда использовать
При submit 'onSubmit' (по умолчанию) Простые короткие формы
При blur (потеря фокуса) 'onBlur' Длинные формы
В реальном времени 'onChange' Пароли, поиск
Смешанный 'all' Показать при blur, обновлять при change

Частые ошибки

Ошибка Почему плохо Как правильно
useState для каждого поля Слишком много ре-рендеров, сложный код Используй React Hook Form / Formik
Нет aria-invalid и aria-describedby Screen reader не узнаёт об ошибке Добавляй ARIA-атрибуты для состояния ошибки
Только клиентская валидация Безопасность нарушена Дублируй схему на сервере (Zod)
Показ всех ошибок сразу при загрузке Агрессивный UX Показывай ошибки только после взаимодействия

Связанные темы

Ресурсы