Розробка форми з валідацією в реальному часі на сайті
Валідація в реальному часі показує помилки одразу при введенні, не дочікуючись відправки форми. Правильно реалізована — поліпшує досвід; неправильно — дратує червоними помилками до того, як користувач закінчив писати.
Стратегія відображення помилок
Не показувати помилки:
- При першому рендерингу
- Поки користувач активно набирає текст (до втрати фокуса)
Показувати помилки:
- Після того як поле втратило фокус (
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(/^\+\d{1,3}\d{6,14}$/, 'Формат: +1234567890'),
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="+1234567890"
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 робочих дні.







