Проектирование экранов в React для состояний загрузки, пустоты, ошибки и повторной попытки
Научитесь моделировать все возможные состояния экрана и формы в React — от структурных основ и повторной загрузки данных до пустых результатов, полезных сообщений об ошибках и защиты от двойной отправки данных.
Источники: Your React App Works. Until the API Doesn’t.
Экран, который идеально работает с локальной базой данных в тестовых условиях, может перестать функционировать в реальных условиях использования, где пользователи сталкиваются с медленными сетями, пустыми таблицами и неисправными конечными точками. В этом руководстве рассматриваются все состояния, через которые проходит React-экран, основанный на данных, и его формы, а также приводится чек-лист для проектирования каждого из них перед написанием кода JSX.
Ментальная модель большинства нестабильных экранов
Когда компонент впервые разрабатывается, его работа обычно представляется как прямая линия: запрашивается данные, затем они отображаются.
Fetch data → Show data
В такой модели нет места для сбоев. Экран в производственных условиях имеет множество вариантов развития событий: он начинает загрузку, может получить успешный результат или пустой результат, а также может возникнуть ошибка; для возврата к нормальной работе требуется возможность повторной попытки или перезагрузки данных.
Loading → Success → Empty
↓
Error
↓
Retry / Refetch
Рассматривайте процесс загрузки как отдельное состояние с собственным дизайном
Фраза «показать индикатор загрузки» звучит как полный ответ, так же как и фраза «обернуть код в try/catch»: это не неправильно, но редко отражает то, что переживают пользователи. Два вопроса выявляют этот разрыв: как выглядит экран, когда ответ генерируется в течение четырех секунд? И что происходит, когда ответ приходит за 40 миллисекунд — видит ли пользователь, как индикатор моргает всего один кадр, и воспринимает ли интерфейс как нестабильный?
Первоначальная загрузка против повторной загрузки
Полезно разделить процесс загрузки на две отдельные ситуации:
- Первоначальная загрузка. На экране пока ничего нет, поэтому использование шаблона, соответствующего окончательному макету, является разумным и даже приятным решением.
- Повторная загрузка. Данные уже видны, например после того, как пользователь нажал кнопку обновления или изменил фильтр. Удаление их и замена полноэкранным индикатором загрузки приводит к потере полезной информации.
Во время повторного загрузчения сохраняйте отрендеренные предыдущие данные и покажите небольшой встроенный индикатор рядом с элементом, который обновляется. Индикаторы загрузки никого не успокаивают; в лучшем случае их терпят. Для очень быстрых ответов рассмотрите возможность кратковременной задержки появления индикатора, чтобы он отображался только тогда, когда ожидание становится заметным.
Почему успешный вызов fetch — это лишь небольшая часть работы
Сам запрос — это самая простая часть:
const res = await fetch("/api/students");
const data = await res.json();
setStudents(data);
Этот фрагмент не отвечает на все вопросы, связанные с ним. Что видит пользователь, пока обещание находится в состоянии ожидания? Что, если fetch вернет ошибку или сервер ответит статусом, отличным от 2xx (который fetch сам по себе не считает ошибкой)? Что, если тело запроса — это пустой массив? Важный вопрос заключается не в том, как получить данные, а в том, какие состояния может иметь этот запрос в любой момент.
Если students — единственный элемент состояния, остальная часть интерфейса должна определять ситуацию на основе того, является ли эта переменная null, пустой или заполненной. Значение null может означать «продолжается загрузка» или «запрос не удался», и нет способа отличить одно от другого. Это просто догадки, маскирующиеся под управление состоянием.
Пустой результат — это успех, а не ошибка
Этот момент стоит выделить, потому что многие проекты ошибочно его понимают. Ответ 200 OK с содержимым [] означает, что сервер выполнил свою работу правильно: он сообщает о том, что показывать нечего.
Фразы «Студентов не найдено» и «Не удалось загрузить студентов» описывают совершенно разные ситуации, однако многие приложения используют для обеих один и тот же серый заместительный элемент. Общий <p>Что-то пошло не так</p> для обоих случаев объединяет две ситуации, требующие разных решений: пустое состояние может побудить пользователя снять фильтр, тогда как состояние ошибки должно объяснить причину сбоя и предложить способ восстановления.
Как должно выглядеть полезное состояние ошибки
Общая фраза «Что-то пошло не так» никому не помогает. Лучшее состояние ошибки:
- информирует пользователя о действии, которое он может предпринять, например о проверке подключения или попытке снова позже;
- предоставляет кнопку повторной попытки, когда есть реальные шансы на успех;
- никогда не отображает в интерфейсе сырые данные о стеке вызовов или внутренний контент ошибки;
- ограничивается той частью экрана, где произошел сбой.
Последний пункт — тот, который чаще всего игнорируют. На панели управления с пятью виджетами один неудачный запрос не должен маскировать четыре, которые загрузились успешно. Обработка ошибок по отдельным разделам через локальное состояние или границы ошибок вокруг независимых областей.
Формы проходят свой собственный жизненный цикл
Запись данных имеет ещё больше состояний, чем их чтение:
Idle → Editing → Submitting → Success / Error
Защита от дублирования отправок
Рассмотрим пользователя, который нажимает кнопку «Отправить» три раза из-за отсутствия видимой обратной связи после первых двух нажатий. Если кнопка не отключена во время отправки данных или обработчик не проверяет флаг isSubmitting и не прерывает работу, когда запрос уже в пути, сервер получит три запроса. В случае формы заказа это означает, что один клиент будет оплачивать одну футболку три раза. При обработке платежей сервер также должен устранять дубликаты, например с использованием ключа идемпотентности.
Сохранение данных пользователя при неудачной отправке
При неудаче запроса форму следует оставить заполненной. Приходится перепечатывать длинное сообщение после сбоя в сети — это тратит время пользователя. Ошибки проверки, будь то правила с клиентской стороны или ошибки сервера, должны отображаться рядом с соответствующим полем, а не в размытом баннере в верхней части формы.
Замените разрозненные логические значения на именованные состояния
Если не применять контроль, компонент склонен накапливать подобные флаги:
isLoading, isError, isSubmitting, hasData, isEmpty
Каждый флаг имеет смысл по отдельности. Вместе пять логических значений позволяют получить 32 комбинации, из которых лишь немногие имеют смысл, и ничто не мешает тому, чтобы isLoading && isError были истинными одновременно.
Странице с списком студентов не нужна формальная библиотека автоматов состояний. Ей необходимо тщательное решение, принятое до первого вызова useState, относительно того, какие состояния может иметь экран. Представьте текущее состояние с помощью одного значения, например поля status, содержащего "loading", "success", "empty" или "error", и позвольте процессу отрисовки определяться этим значением, вместо того чтобы воссоздавать ситуацию на основе нескольких независимых флагов. В TypeScript дискриминированное объединение делает невозможными комбинации несущественными; статья о типизации useState, useReducer и пользовательских хуков более подробно рассматривает аспекты типизации.
Чек-лист перед созданием следующего экрана
Ответьте на эти вопросы перед тем, как написать строку return в компоненте:
- Что отображается до поступления данных?
- Что происходит, если запрос завершается с ошибкой?
- Что происходит, если запрос успешен, но ничего не возвращается?
- Что видит пользователь во время обновления существующих данных?
- Что происходит при отправке формы?
- Что происходит, если форма отправляется дважды?
- Что происходит, если проверка данных завершается с ошибкой?
Заключение
Разница между демо-версией и производственным интерфейсом редко заключается в успешном выполнении операций. Она проявляется в состояниях загрузки, пустоты, ошибок, повторного обновления и отправки данных, которые появляются сразу, как в дело вступают реальные пользователи и реальные сети. Явно опишите эти состояния, тщательно спроектируйте каждое из них, ограничьте последствия сбоев, а также защитите ввод пользователей и действия сервера от повторных или неудачных отправок.