Розробка екранів React для станів завантаження, порожнього стану, помилки та повторної спроби
Навчіться моделювати кожен можливий стан екрана та форми в React — від початкових станів та перезавантажень до порожніх результатів, корисних помилок та захисту від подвійної передачі даних.
Екран, який ідеально працює з локальною базо даних, все одно може припинити функціонувати в умовах реального використання, де користувачі стикаються з повільними мережами, порожніми таблицями та несправними кінцевими точками. У цьому посібнику розглядаються стани, через які проходить даними керований екран 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 дискримінована unія робить неможливі комбінації непредставленими; стаття типування useState, useReducer та власних гаків детальніше розглядає аспекти типування.
Чек-лист для виконання перед створенням наступного екрана
Відповідайте на ці запитання перед тим, як написати return у компоненті:
- Що видно до того, як надійдуть дані?
- Що відбувається, якщо запит зазнає невдачі?
- Що відбувається, якщо запит успішний, але нічого не повертає?
- Що бачить користувач під час оновлення наявних даних?
- Що відбувається під час надсилання форми?
- Що відбувається, якщо форма надсилатиметься двічі?
- Що відбувається, якщо перевірка даних зазнає невдачі?
Підсумок
Різниця між демо-версією та продакшн-інтерфейсом рідко полягає у «ідеальному» сценарії. Вона проявляється у станах завантаження, порожнього екрану, помилок, оновлення даних та надсилання запитів, які з’являються щойно до гри вступають реальні користувачі та мережі. Очевидно позначте ці стани, ретельно спроєктуйте кожен з них, обмежуйте наслідки невдач, а також захищайте введені користувачем дані та дії сервера від повторних чи невдалих запитів.