Представь: пользователь вводит email в форму регистрации, ты делаешь async-проверку на бекенде, и внезапно интерфейс начинает дергаться. Кнопка пляшет по ширине, ошибка выталкивает контент, скринридер молчит. Знакомо? Давай разбираться, почему так происходит и как это чинить.
Проблема: валидация ломает UX
Классический антипаттерн — рендерить сообщение об ошибке только при её появлении:
{error && <p className="error">{error}</p>}
Проблемы такого подхода:
- Layout shift: сообщение появляется и сдвигает всё ниже
- Бешеный кнопки: индикатор загрузки меняет ширину submit-кнопки
- Accessibility hell: скринридер не знает о статусе проверки
На мобиле это выглядит особенно криво — пользователь теряет контекст, когда контент прыгает под пальцами.
Четыре состояния вместо двоичной логики
Первое решение — явно смоделировать состояния валидации:
const [status, setStatus] = useState<'idle' | 'checking' | 'valid' | 'invalid'>('idle');
const [message, setMessage] = useState('');
async function validateEmail() {
setStatus('checking');
setMessage('Проверяем email...');
const result = await api.checkEmail(email);
setStatus(result.ok ? 'valid' : 'invalid');
setMessage(result.ok ? 'Email валиден' : 'Неправильный формат');
}
Особенно важен статус checking — без него пользователь может отправить форму несколько раз, а интерфейс не объяснит, какое сообщение относится к какому запросу.
CSS: резервируем место заранее
Чтобы сообщение не сдвигало layout, резервируем для него место в любом состоянии:
.field-feedback {
min-height: 24px; /* Фиксируем высоту */
margin-top: 6px;
visibility: hidden; /* Элемент занимает место, но невидим */
}
.field-feedback[data-status] {
visibility: visible; /* Показываем при любом статусе */
}
[data-status="invalid"] {
color: var(--error);
}
[data-status="valid"] {
color: var(--success);
}
В JSX добавляем data-status и ARIA-атрибуты:
<p
className="field-feedback"
data-status={status}
aria-live="polite" /* Скринридер объявит изменение */
id="email-feedback"
>
{message}
</p>
Не доверяй только цветам
Красный текст ошибки — недостаточно. Для скринридеров явно помечаем невалидное поле:
<input
aria-invalid={status === 'invalid'}
aria-describedby="email-feedback" /* Связываем с сообщением */
/>
aria-live="polite" заставляет скринридер объявить изменение, но без резких перехватов фокуса.
Когда что отменять
Важные нюансы async-валидации:
- Debounce: не дёргаем API на каждый keystroke, 300-500ms достаточно
- AbortController: отменяем предыдущий запрос при новом вводе
- Race conditions: используем ID запроса или флаги, чтобы старые ответы не перезаписывали новые
Для кнопки submit:
- Либо делаем её фиксированной ширины
- Либо резервируем место под spinner
- Либо заменяем текст на “Проверяем…”
Checklist перед продакшеном
Перед выкаткой пройдись по чеклисту:
- Сообщение всегда занимает одинаковую высоту
- Скринридер объявляет изменения статуса
- Нет layout shift при появлении ошибки
- Кнопка не дёргается при загрузке
- Цвет — не единственный индикатор ошибки
- Пользователь понимает, что проверка идёт
Этих нехитрых правил хватит, чтобы forms-логика не превращалась в аттракцион. Главное — думать не только о том, “как отправить данные”, но и о том, как это выглядит со стороны.
Источник: https://dev.to/silviutech/react-validacion-async-sin-saltos-visuales-41ja