Strona główna / Artykuły

Projektowanie ekranów w React dla stanów ładowania, pustego stanu, błędu i ponownej próby

Naucz się modelować każdy możliwy stan ekranu i formy w React – od struktur początkowych i ponownych pobierania danych, przez puste wyniki, przydatne komunikaty błędów, aż po ochronę przed podwójnym wysyłaniem danych.

Wydawca: REACTAPP.TOP6 min czytania1319 słów

Źródła: Your React App Works. Until the API Doesn’t.

Ekran, który działa perfekcyjnie przy użyciu lokalnej bazy danych, może nadal zawieść w środowisku produkcyjnym, gdzie użytkownicy borykają się z wolnymi łączami, pustymi tabelami oraz niepracującymi punktami końcowymi. Ten przewodnik opisuje wszystkie etapy, przez które przechodzi ekran w Reactzie oparty na danych oraz jego formularze, i dostarcza listę kontrolną do zaprojektowania każdego z nich przed napisaniem jakiegokolwiek kodu JSX.

Model mentalny większości kruchych ekranów

Gdy komponent jest po raz pierwszy projektowany, jego działanie wygląda jak prosta linia: wysyła się żądanie, a następnie renderuje wynik.

Fetch data → Show data

W tym modelu nie ma miejsca na błędy. Ekran w środowisku produkcyjnym ma wiele możliwych scenariuszy: zaczyna ładowanie, może osiągnąć sukces lub pusty wynik, może również napotkać błąd i wymaga sposobu na powrót poprzez ponowną próbę lub pobranie danych.

Loading → Success → Empty
             ↓
           Error
             ↓
        Retry / Refetch

Traktuj proces ładowania jako odrębny stan z własnym projektem

"Pokazać spinner" brzmi jak kompletna odpowiedź, podobnie jak "opakować to w try/catch": nie jest to błędne, ale rzadko odzwierciedla to to, co doświadczają użytkownicy. Dwie pytania ujawniają tę lukę. Jak wygląda ekran, gdy odpowiedź przychodzi po czterech sekundach? A co się dzieje, gdy przychodzi w ciągu 40 milisekund – czy użytkownik widzi migający spinner przez jeden kadrowanie i postrzega interfejs jako niestabilny?

Pierwotne ładowanie versus ponowne pobranie

Pomaga podzielić proces ładowania na dwie odrębne sytuacje:

  • Pierwotne ładowanie. Na ekranie jeszcze nic nie jest, więc szkielet odzwierciedlający ostateczną strukturę jest rozsądnym, a nawet przyjemnym wyborem.
  • Ponowne pobranie. Dane są już widoczne, na przykład po naciśnięciu przycisku odświeżenia lub zmianie filtra. Usunięcie ich i zastąpienie pełnym elementem ładowania niszczy przydatny kontekst.

Dlaczego skuteczna funkcja fetch to tylko niewielka część zadania

Sama prośba o dane to najłatwiejsza część:

const res = await fetch("/api/students");
const data = await res.json();
setStudents(data);

fetch odrzuci żądanie lub serwer odpowie statusem innym niż 2xx (który sam w sobie nie jest przez fetch traktowany jako błąd)? A co, jeśli treść to pusta tablica? Ważne pytanie nie brzmi, jak uzyskać dane, ale w jakich stanach może się znajdować to żądanie w dowolnym momencie.

Jeśli students jest jedynym elementem stanu, reszta interfejsu musi wywnioskować sytuację na podstawie tego, czy ta zmienna ma wartość null, jest pusta, czy zawiera dane. null może oznaczać „nadal ładowane” lub „prośba nie powiodła się”, a nie ma sposobu, aby je odróżnić. To domysły udające zarządzanie stanem.

Pusta odpowiedź to sukces, a nie błąd

Tę kwestię warto wyodrębnić, ponieważ wiele baz kodu błędnie ją interpretuje. Odpowiedź 200 OK zawierająca [] oznacza, że serwer wykonał swoją pracę prawidłowo – informuje o tym, że nie ma nic do pokazania.

"Brak znalezionych uczniów" oraz "Nie udało się załadować uczniów" opisują zupełnie różne sytuacje, a mimo to wiele aplikacji używa dla obu tych przypadków tego samego szarego pola zastępczego. Używanie wspólnego <p>Coś poszło nie tak</p> dla obu przypadków łączy dwie różne sytuacje wymagające odmiennych rozwiązań: stan pusty może zachęcać użytkownika do usunięcia filtru, natomiast stan błędu powinien wyjaśnić przyczynę problemu i zaproponować sposób naprawy.

Jak powinien wyglądać przydatny stan błędu

Ogólne powiadomienie "Coś poszło nie tak" nikomu nie pomaga. Lepszy stan błędu:

  • poinformuje użytkownika o czymś, co może zrobić, np. sprawdzić połączenie lub spróbować ponownie później;
  • zaoferuje przycisk ponownej próby, gdy istnieje realna szansa na jej powodzenie;
  • nigdy nie pokazuje surowego śladu wywołania ani danych błędu w interfejsie użytkownika;
  • pozostaje ograniczony do tej części ekranu, w której wystąpił błąd.

Ostatni element jest tym, który najczęściej pomija się. Na panelu sterowania z pięcioma elementami interfejsu jeden nieudany żądanie nie powinien maskować czterech, które zostały pobraane pomyślnie. Obsługa błędów powinna być zastosowana na poziomie poszczególnych sekcji, poprzez lokalny stan lub granice błędów wokół niezależnych obszarów.

Formularze przechodzą przez swój własny cykl życiowy

Zapisywanie danych ma jeszcze więcej stanów niż ich odczytywanie:

Idle → Editing → Submitting → Success / Error

Ochrona przed podwójnym wysyłaniem

Rozważmy użytkownika, który kliknie przycisk „Zatwierdź” trzy razy, ponieważ po pierwszych dwóch kliknięciach przycisk nie dawał żadnej widocznej informacji zwrotnej. Chyba że przycisk jest wyłączony podczas wysyłania danych lub obsługujący go kod sprawdza flagę isSubmitting i wcześnie zwraca się, gdy żądanie jest już w trakcie przetwarzania, serwer otrzymuje trzy żądania. W przypadku formy zamówień oznacza to, że jeden klient zostaje obciążony kosztami trzykrotnie za jedną koszulkę. W przypadku płatności backend powinien również usuwać duplikaty, na przykład za pomocą klucza idempotencji.

Zachowywanie danych wprowadzonych przez użytkownika w przypadku niepowodzenia wysyłki

Gdy żądanie się nie powiedzie, należy pozostawić formę wypełnioną. Zmuszanie kogoś do ponownego wpisywania długiej treści po awarii sieci marnuje jego czas. Błędy walidacji, niezależnie od tego, czy pochodzą z reguł po stronie klienta, czy ze strony serwera, powinny być pokazywane obok pola, do którego odnoszą się, a nie w ogólnikowym komunikacie u góry formy.

Zastąp rozproszone wartości booleanowe nazwanymi stanami

Jeśli nie zostaną sprawdzone, komponent ma tendencję do gromadzenia takich flag:

isLoading, isError, isSubmitting, hasData, isEmpty

Każda flaga ma sens samodzielnie. Razem pięć wartości booleanowych umożliwia 32 kombinacje, z których tylko kilka ma znaczenie, a nic nie przeszkadza temu, by isLoading && isError było prawdziwe w obu przypadkach.

Strona z listą studentów nie wymaga formalnej biblioteki do zarządzania stanami. Potrzebuje natomiast świadomej decyzji, podjętej przed pierwszym wywołaniem useState, dotyczącej tego, jakie konkretne stany może przyjmować ekran. Obrazuj bieżący stan za pomocą pojedynczej wartości, np. pola status zawierającego "loading", "success", "empty" lub "error", i pozwól, by renderowanie zależało od tej wartości, zamiast odtwarzać sytuację na podstawie kilku niezależnych flag. W TypeScript rozwiązaniem jest zjednoczenie dyskryminowane, które uniemożliwia reprezentację niemożliwych kombinacji; artykuł na temat typowania useState, useReducer oraz własnych hooków omawia ten aspekt bardziej szczegółowo.

Lista kontrolna przed stworzeniem kolejnego ekranu

Odpowiedz na te pytania przed napisaniem instrukcji return w komponencie:

  1. Co jest widoczne zanim dotrą jakiekolwiek dane?
  2. Co się dzieje, gdy żądanie zawodzi?
  3. Co się dzieje, gdy udaje się je zrealizować, ale nie zwraca żadnych danych?
  4. Co widzi użytkownik podczas odświeżania istniejących danych?
  5. Co się dzieje podczas wysyłania formularza?
  6. Co się dzieje, jeśli formularz zostanie wysłany dwa razy?
  7. Co się dzieje, gdy walidacja zawodzi?

Podsumowanie

Różnica pomiędzy demonstracją a interfejsem produkcyjnym rzadko leży w „szczęśliwej ścieżce”. Polega ona na stanach ładowania, pustki, błędu, ponownego pobierania danych oraz wysyłania, które pojawiają się w momencie zaangażowania rzeczywistych użytkowników i sieci. Określ te stany wyraźnie, zaprojektuj każdy z nich starannie, utrzymuj błędy w lokalnym zakresie oraz chronij dane wprowadzone przez użytkownika oraz efekty działania serwera przed powtarzającymi się lub nieudanymi próbami wysyłania.

Literatura pokrewna

  • Projektowanie dla nieudanych zapytań w Angular: stany, interceptory i próby ponowne — Dowiedz się, jak klasyfikować błędy HTTP w Angular, oczyszczać stan ładowania, centralizować obsługę w interceptorych, bezpiecznie próbować ponownie oraz wyświetlać komunikaty, na które użytkownicy mogą zareagować.
  • Ładowanie, czekanie, błędy: jak lazy, Suspense i Error Boundaries się dopasowują — Zrozum, jak React.lazy, Suspense i Error Boundaries dzielą kod, pokazują alternatywy i zapobiegają awariom, oraz gdzie umieścić każdy z tych elementów dla lepszej obsługi użytkownika.