Разработка формы с валидацией в реальном времени на сайте
Валидация в реальном времени показывает ошибки сразу при вводе, не дожидаясь отправки формы. Правильно реализованная — улучшает опыт; неправильно — раздражает красными ошибками до того, как пользователь закончил печатать.
Стратегия отображения ошибок
Не показывать ошибки:
- При первоначальном рендере
- Пока пользователь активно набирает текст (до потери фокуса)
Показывать ошибки:
- После того как поле потеряло фокус (
onBlur) - Если поле уже было посещено и значение изменилось
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const schema = z.object({
email: z.string().email('Некорректный email'),
phone: z.string().regex(/^\+7\d{10}$/, 'Формат: +7XXXXXXXXXX'),
password: z.string()
.min(8, 'Минимум 8 символов')
.regex(/[A-Z]/, 'Нужна хотя бы одна заглавная буква')
.regex(/\d/, 'Нужна хотя бы одна цифра'),
});
export function RegistrationForm() {
const { register, handleSubmit, formState: { errors, touchedFields } } = useForm({
resolver: zodResolver(schema),
mode: 'onBlur', // валидация при потере фокуса
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<ValidatedInput
label="Email"
error={errors.email?.message}
touched={touchedFields.email}
{...register('email')}
/>
<ValidatedInput
label="Телефон"
placeholder="+7XXXXXXXXXX"
error={errors.phone?.message}
touched={touchedFields.phone}
{...register('phone')}
/>
</form>
);
}
function ValidatedInput({ label, error, touched, ...props }) {
const hasError = touched && error;
return (
<div className="mb-4">
<label className="block text-sm font-medium mb-1">{label}</label>
<input
{...props}
className={cn('input-field', hasError && 'border-red-500 focus:ring-red-500')}
/>
{hasError && <p className="text-red-500 text-xs mt-1">{error}</p>}
{touched && !error && <p className="text-green-500 text-xs mt-1">✓</p>}
</div>
);
}
Асинхронная валидация
Проверка уникальности email с дебаунсом (не отправлять запрос на каждое нажатие):
const emailExists = useCallback(
debounce(async (email: string) => {
if (!email || !/\S+@\S+/.test(email)) return;
const resp = await fetch(`/api/check-email?email=${encodeURIComponent(email)}`);
const { exists } = await resp.json();
if (exists) setError('email', { message: 'Этот email уже зарегистрирован' });
else clearErrors('email');
}, 500),
[]
);
Индикатор силы пароля
function PasswordStrength({ password }: { password: string }) {
const checks = [
{ label: 'Минимум 8 символов', pass: password.length >= 8 },
{ label: 'Заглавная буква', pass: /[A-Z]/.test(password) },
{ label: 'Цифра', pass: /\d/.test(password) },
{ label: 'Спецсимвол', pass: /[!@#$%^&*]/.test(password) },
];
const score = checks.filter(c => c.pass).length;
return (
<div className="mt-2">
<div className="flex gap-1 mb-2">
{[1,2,3,4].map(i => (
<div key={i} className={cn('h-1 flex-1 rounded', i <= score ? strengthColors[score] : 'bg-gray-200')} />
))}
</div>
<ul className="space-y-1">
{checks.map(check => (
<li key={check.label} className={cn('text-xs flex items-center gap-1', check.pass ? 'text-green-600' : 'text-gray-400')}>
{check.pass ? '✓' : '○'} {check.label}
</li>
))}
</ul>
</div>
);
}
Время реализации: 2–3 рабочих дня.







