Real-Time Validation Form Development
Real-time validation shows errors immediately on input, without waiting for form submission. Properly implemented — improves experience; improperly — frustrates with red errors before user finishes typing.
Error Display Strategy
Don't show errors:
- On initial render
- While user actively typing (until blur)
Show errors:
- After field lost focus (
onBlur) - If field already visited and value changed
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const schema = z.object({
email: z.string().email('Invalid email'),
phone: z.string().regex(/^\+\d{1,3}\d{6,14}$/, 'Format: +1234567890'),
password: z.string()
.min(8, 'Minimum 8 characters')
.regex(/[A-Z]/, 'Need uppercase letter')
.regex(/\d/, 'Need digit'),
});
export function RegistrationForm() {
const { register, handleSubmit, formState: { errors, touchedFields } } = useForm({
resolver: zodResolver(schema),
mode: 'onBlur', // validate on blur
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<ValidatedInput
label="Email"
error={errors.email?.message}
touched={touchedFields.email}
{...register('email')}
/>
<ValidatedInput
label="Phone"
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>
);
}
Async Validation
Check email uniqueness with debounce (don't send request on every keystroke):
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: 'This email is already registered' });
else clearErrors('email');
}, 500),
[]
);
Password Strength Indicator
function PasswordStrength({ password }: { password: string }) {
const checks = [
{ label: 'Minimum 8 characters', pass: password.length >= 8 },
{ label: 'Uppercase letter', pass: /[A-Z]/.test(password) },
{ label: 'Digit', pass: /\d/.test(password) },
{ label: 'Special character', 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>
);
}
Implementation time: 2–3 working days.







