Strona główna / Artykuły

Ile kosztuje cię używanie useEffect do pobierania danych i jakie zmiany wprowadza TanStack Query

Dlaczego pobieranie danych oparte na efektach powoduje duplikowane żądania, szablonową kodologię oraz przestarzałe zamykania w React, i jak klucze, staleTime oraz mechanizm unieważniania w TanStack Query rozwiązują te problemy.

Wydawca: REACTAPP.TOP6 min czytania1360 słów

Źródła: Stop Using useEffect for API Calls, Use React Query Instead

Ładowanie danych wewnątrz useEffect to pierwszy wzorzec, który uczą się większość deweloperów React, i sprawdza się dobrze przy pojedynczej prośbie na jednej stronie. Gdy aplikacja rośnie, ten sam wzorzec potajemnie mnoży prośby, powtarza szablonowy kod we wszystkich komponentach i sprzyja subtelnym błędom związanym z zależnościami. Ten artykuł wyjaśnia, skąd pochodzą te koszty oraz jak TanStack Query (wcześniej React Query) radzi sobie z każdym z nich, abyś mógł zdecydować, kiedy dokonać zmiany i co skonfigurować po jej przeprowadzeniu.

Wzorzec, od którego zaczynają wszyscy

Znany kształt to efekt z pustym tablicą zależności, który wywołuje funkcję fetch po załadowaniu komponentu:

useEffect(() => {
  fetchData();
}, []);

Wydaje się to proste, a właśnie to jest częścią problemu. Kod nie mówi nic o buforowaniu, usuwaniu duplikatów, ponawianiu prób, anulowaniu ani o tym, co powinno się stać, gdy komponent zostanie ponownie załadowany. Każda z tych luk przekształca się w błąd lub dodatkowy żądanie, gdy jest już kilkudziesięt punktów końcowych oraz większa liczba stanów ładowania i błędów do śledzenia.

Dlaczego żądanie jest wysyłane dwa razy i jaki jest prawdziwy problem

Pierwszym częstym zaskoczeniem jest widok tego samego żądania dwa razy w karcie sieci podczas ładowania strony. W środowisku rozwojowym StrictMode w React celowo montuje, demontuje i ponownie montuje komponenty, aby ujawnić efekty, które same się nie usuwają. Podwójne żądanie jest zamierzone w środowisku rozwojowym i nie występuje w wersji produkcyjnej.

Dwukrotne wywołanie jest objawem problemu, który ma znaczenie w produkcji: żądanie oparte na efektach jest powiązane z cyklem życia komponentu. Każde uruchomienie komponentu wywołuje nowe żądanie, nawet gdy dane są już dostępne. Komponenty, które są uruchamiane wielokrotnie, takie jak panele kart, okna modalne i wiersze listy, ciągle wysyłają żądania do serwera, zwiększając jego obciążenie i sprawiając, że interfejs wydaje się wolniejszy, niż powinien być.

TanStack Query oddziela dane od komponentu. Wyniki są przechowywane w wspólnym buforze pod kluczem zapytania. Gdy inny komponent lub ten sam po ponownym uruchomieniu poprosi o klucz, którego dane są nadal uważane za aktualne, biblioteka zwraca wartość z bufora bez korzystania z sieci. Kilka komponentów żądających tego samego klucza w tym samym momencie dzieli się również jednym żądaniem w trakcie przetwarzania.

Ręczne zarządzanie stanem ładowania i błędów

Za pomocą useEffect każda prośba wymaga własnego zestawu trzech zmiennych stanu: danych, flagi ładowania oraz informacji o błędzie:

const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

Jeśli powtarza się to przy każdej wywołaniu API, powstaje dużo niemal identycznego kodu, przy czym każda kopia niesie ze sobą ryzyko błędu. Najczęstszym problemem jest zapomnienie o sformatowaniu zmiennej loading w przypadku błędu, co powoduje, że ikona spinowania nie przestaje się obracać. Inne problemy to brak ignorowania odpowiedzi przychodzącej po rozmontowaniu komponentu oraz sytuacja, gdy powolna odpowiedź z wcześniejszego wywołania przykrywa nowszą, gdy dane są zmieniane szybko.

useQuery zwraca bezpośrednio data, error, isLoading oraz isError (wraz z bardziej szczegółowymi polami stanu), a także sam zajmuje się obsługą odpowiedzi o niewłaściwej kolejności. Kod szablonowy znika z komponentów, a przypadki krawędziowe są rozwiązywane raz w dobrze przetestowanej bibliotece, zamiast wielokrotnie w kodzie aplikacji.

Tablice zależności versus klucze zapytań

Efekty zależą od prawidłowego ustawienia tablicy zależności. Jeśli pominiesz wartość, którą efekt ma odczytać, powstanie przestarzała zamykana funkcja, która wysyła żądania z nieaktualnymi parametrami – to błąd, którego wykrycie może zająć godziny. Jeśli zaś dodasz obiekt lub funkcję, która jest tworzona na nowo przy każdym renderowaniu, efekt będzie uruchamiany przy każdym renderowaniu i wysyłał żądanie za każdym razem.

TanStack Query zastępuje to domyślanie się jaśniejszą umową: przypisujesz każdej zapytaniu klucz oraz funkcję pobierania danych. Klucz powinien zawierać wszystkie zmienne, od których zależy żądanie – na przykład nazwę zasobu wraz z identyfikatorem użytkownika lub filtrem. Gdy klucz się zmienia, uruchamiane jest nowe zapytanie; jeśli pozostaje niezmieniony, wykorzystywane są dane z pamięci cache. Klucze porównuje się pod względem ich zawartości, a nie adresu pamięci, więc tworzenie nowego array przy każdym renderowaniu nie powoduje dodatkowych pobrań danych. Ponowne pobieranie jest wtedy kontrolowane przez wyraźne zasady, takie jak montaż komponentu, przywrócenie uwagi okna lub ponowne połączenie sieci, w połączeniu z ustawionym parametrem staleTime.

Nadal musisz myśleć o zależnościach, ale wyrażasz je raz w formie klucza, zamiast utrzymywać array, który musi być zsynchronizowany z treścią efektu.

Pamięć cache, staleTime i unieważnianie

Trzy funkcje odpowiadają za większość różnic w codziennej pracy.

Cache

Po pobraniu danych do klucza, pozostają one w cache’u. Gdy użytkownik odejdzie i wróci, zapisany wynik jest wyświetlany natychmiast, a jeśli dane są przestarzałe, biblioteka pobiera je ponownie w tle i aktualizuje ekran po otrzymaniu nowej odpowiedzi.

staleTime

staleTime określa, jak długo dane są uważane za świeże. Dopóki są świeże, ponowne załadowanie oraz nowi subskrybenci w ogóle nie wywołują żadnego żądania. Domyślna wartość to zero, co oznacza, że dane są natychmiast traktowane jako przestarzałe; zapisane dane nadal są wyświetlane błyskawicznie, ale ponowne pobranie w tle następuje przy kolejnym załadowaniu lub skupieniu uwagi na oknie. Ustawienie rozsądnego wartości staleTime dla danych, które nie zmieniają się co sekundę, często przynosi największe zmniejszenie ruchu w tle.

invalidateQueries

Po mutacji, takiej jak aktualizacja profilu lub tworzenie posta, wywołuje się queryClient.invalidateQueries z odpowiednim kluczem. Pasujące zapytania są oznaczane jako przestarzałe, a te, które są obecnie w użyciu, są automatycznie ponownie pobierane. Nie ma konieczności ręcznego ponownego pobierania danych po każdej mutacji ani ręcznego kopiowania odpowiedzi serwera do lokalnego stanu.

Wspólnie te trzy elementy pokrywają większość problemów związanych z pobieraniem danych w oparciu o efekty. Aby dowiedzieć się więcej na temat organizacji kluczy, opcji zapytań i mutacji w dużych skali, zapoznaj się z naszym przewodnikiem strukturyzacji warstwy danych TanStack Query.

Typowy scenariusz migracji

Rozważmy panel sterowania typu SaaS w Next.js z kilkoma kartami, z których każda ładuje własne dane za pomocą useEffect. Ponieważ zmiana karty powoduje ponowne załadowanie paneli, każda taka zmiana wysyła nową serię żądań. Serwer otrzymuje znacznie więcej ruchu, niż to konieczne, a użytkownicy zauważają wolniejsze ładowanie.

Migracja zazwyczaj polega na trzech krokach:

  1. Zastąp każdy efekt funkcją useQuery wraz z kluczem, który przechowuje wszystkie parametry żądania.
  2. Ustaw dla każdego zapytania wartość staleTime, która odzwierciedla częstotliwość rzeczywistych zmian danych.
  3. Po każdej mutacji unieważnij klucze, których dane zostały zmienione.

Oczekiwany rezultat to płynniejszy panel sterowania, mniej wywołań serwera oraz lepsze doświadczenie użytkowników, przy mniejszej ilości kodu w każdym komponencie.

Gdzie nadal ma zastosowanie useEffect

To nie jest przypadek, w którym należałoby całkowicie usunąć useEffect. Nadal jest to odpowiedni narzędzie do synchronizacji z elementami poza Reactem, które nie są danymi typu żądanie-odpowiedź: subskrypcje, słuchacze zdarzeń DOM, timerzy oraz komponenty od dostawców trzecich. Jednak w przypadku wywołań API w aplikacji, która się rozwija, lepszym rozwiązaniem jest dedykowana biblioteka do zarządzania stanem serwera, ponieważ zmniejsza obciążenie backendu, eliminuje kod typu boilerplate oraz oferuje funkcje cache’owania i unieważniania danych od razu po użyciu.

Istnieją również przypadki, gdy biblioteka jest bardziej niż potrzebna, na przykład w małej aplikacji z jednym lub dwoma żądaniami. W takich sytuacjach wystarczy niewielki, dostosowany hook z odpowiednią funkcją anulowania; nasz artykuł na temat minimalnego hooka useFetch i jego ograniczeń omawia ten kompromis. Jeśli nie jesteś pewien, najpierw przenieś jedną stronę i porównaj liczbę żądań w karcie sieci.

Główne wnioski

  • Dwukrotne wysyłanie żądań w fazie rozwoju wynika z StrictMode; problemem w produkcji jest to, że żądania oparte na efektach są powtarzane przy każdym uruchomieniu.
  • Klucz zapytania zastępuje tablicę zależności: należy uwzględnić wszystkie dane, od których zależy żądanie, a dzięki temu możliwe staje się cacheowanie i ponowne pobieranie danych.
  • Domyślna wartość staleTime wynosi zero, więc ustaw ją celowo, jeśli chcesz uniknąć tła ponownych pobierań danych przy ponownym uruchomieniu.
  • Anuluj powiązane klucze po mutacjach zamiast ręcznie ponownie pobierać lub synchronizować stan.
  • Używaj useEffect do synchronizacji z zewnętrznymi systemami, a nie do ładowania danych serwera.
  • Powiązane materiały