Валидация форм в 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 | Показывай ошибки только после взаимодействия |
Связанные темы
- Валидация на клиенте и сервере
- Валидация данных -- Joi, Zod
- Формы в HTML
- Фокус и клавиатурная навигация
- _MOC JavaScript
- _MOC HTML