React: async-валидация без дерганий интерфейса

#react#forms#a11y#ux

Представь: пользователь вводит email в форму регистрации, ты делаешь async-проверку на бекенде, и внезапно интерфейс начинает дергаться. Кнопка пляшет по ширине, ошибка выталкивает контент, скринридер молчит. Знакомо? Давай разбираться, почему так происходит и как это чинить.

Проблема: валидация ломает UX

Классический антипаттерн — рендерить сообщение об ошибке только при её появлении:

{error && <p className="error">{error}</p>}

Проблемы такого подхода:

  1. Layout shift: сообщение появляется и сдвигает всё ниже
  2. Бешеный кнопки: индикатор загрузки меняет ширину submit-кнопки
  3. 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-валидации:

Для кнопки submit:

Checklist перед продакшеном

Перед выкаткой пройдись по чеклисту:

Этих нехитрых правил хватит, чтобы forms-логика не превращалась в аттракцион. Главное — думать не только о том, “как отправить данные”, но и о том, как это выглядит со стороны.


Источник: https://dev.to/silviutech/react-validacion-async-sin-saltos-visuales-41ja