Strona główna / Artykuły / React Query i Redux: Nowe podejście do stanu serwera w dużych aplikacjach

React Query i Redux: Nowe podejście do stanu serwera w dużych aplikacjach

Dowiedz się, dlaczego aplikacja do czatowania używała TanStack Query zamiast Redux do zarządzania danymi serwera, oraz gdzie Redux nadal ma zastosowanie w nowoczesnej architekturze React.

2701 słów

Załóżmy, że programista przechodzi z mniejszych projektów do firmy opartej na produktach i jest bardzo ciekawy, jak faktycznie buduje się oprogramowanie na dużą skalę, przeznaczone do użycia w produkcji. Po latach pracy nad aplikacjami frontendowymi dołączenie do zespołu odpowiedzialnego za oprogramowanie używane przez miliony ludzi zmienia sposób, w jaki ocenia się kod.

Przestaje się pytać po prostu: „Czy ta funkcja działa?”

Zamiast tego zaczyna się pytać:

Jak ta architektura radzi sobie przy milionach użytkowników? Jak utrzymuje się synchronizację stanu w całej aplikacji? Co się dzieje, gdy dziesięć różnych komponentów potrzebuje tych samych danych? Jak inżynierowie utrzymują tak dużą bazę kodu w dobrym stanie, gdy produkt się rozwija?

A teraz wyobraźmy sobie, że temu programiście przydzielono jeden z kluczowych modułów produktu – aplikację do czatowania, podobną w duchu do Slacka.

To nie jest drobna funkcja ukryta w jakimś kącie aplikacji.

Doświadczenie czatowe stanowi serce produktu, służąc milionom użytkowników i będąc ściśle powiązanym z kluczowymi procesami biznesowymi, elementami generującymi przychody oraz niektórymi z najważniejszych klientów korporacyjnych firmy.

Dlatego jest rozsądne, że podczas pierwszego zagłębiania się w bazę kodu ten programista miał dość przewidywalne oczekiwania.

Aplikacja czatowa w takim skali nieuchronnie musi radzić sobie z ogromną ilością informacji, które muszą być udostępniane na wielu ekranach: wątki i ich wiadomości, liczba niewyprzeczytanych wiadomości, osoby biorące udział w rozmowie, przewijanie historii, edycje i inne operacje zapisu, wskaźniki ładowania oraz aktualizacje przychodzące w czasie rzeczywistym.

Biorąc to wszystko pod uwagę, można się spodziewać obecności typowych elementów w dużej bazie kodu React:

Redux, Zustand albo przynajmniej jakaś forma globalnego zarządzania stanem.

Jednak po przeszukaniu kodu nie znajduje się żadnego magazynu stanu.

Brak konfiguracji Redux.

Brak Zustandu.

Brak rozbudowanego obiektu Context przechowującego wspólne dane aplikacji.

Na pierwszy rzut oka łatwo byłoby założyć, że podczas przeglądania repozytorium po prostu coś przeoczono.

Jednak głębsze badania ujawniają to, co faktycznie napędza warstwę wspólnych danych.

TanStack Query.

To naprawdę zaskakujące odkrycie, jeśli twoje wyobrażenie o tej bibliotece jest ograniczone.

Wielu programistów postrzega React Query głównie jako narzędzie do pobierania danych z API, przechowywania w pamięci cache odpowiedzi, śledzenia stanów ładowania oraz ponownego pobierania danych w razie zmian.

Jednak w tej aplikacji klasy produkcyjnej obsługującej miliony użytkowników cache zapytań pełnił znacznie więcej funkcji. Służył jako wspólna baza dla wszystkich danych należących do serwera w całej aplikacji.

To spostrzeżenie kwestionuje długo utrzymywaną założenie dotyczące architektury frontendu.

Być może właściwe pytanie nie brzmi:

"Dlaczego tutaj nie ma sklepu Redux?"

Być może powinno brzmieć:

"Dlaczego dane należące do serwera w ogóle musiałyby znajdować się w Redux?"

To pytanie otwiera znacznie głębszą dyskusję na temat tego, jak modelujemy stan w aplikacjach React, oraz dlaczego w wielu systemach rzeczywistych TanStack Query może skutecznie usunąć zaskakująco dużą część mechanizmów zarządzania stanem globalnym, które zespoły tradycyjnie budowały ręcznie.

Od Redux do React Query

Był taki okres, gdy podłączenie wywołania API do aplikacji React wydawało się prostym zadaniem.

Następnie pojawił się Redux.

Samo wywołanie API pozostało proste. Wszystko, co się wokół niego znajdowało, stało się skomplikowane.

Należało zdefiniować akcję.

Napisać reduktora.

Dodać flagi oznaczające ładowanie i błędy.

Rozesłać akcję.

Zapisać odpowiedź w sklepie danych.

Napisać selektor, aby ją ponownie odczytać.

Podłączyć komponent do wszystkich tych elementów.

A potem, po tygodniach lub miesiącach, ktoś nieuchronnie pyta:

">Dlaczego te dane wyglądają na przestarzałe?"

A rozwiązaniem jest zwykle kolejna akcja, mająca na celu wymuszenie ponownego pobrania danych.

Po wielokrotnym przechodzeniu przez ten cykl staje się oczywisty nieprzyjemny wzorzec:

Narzędzia do zarządzania stanem globalnym były często używane do obsługi czegoś, co od samego początku wcale nie stanowiło problemu związanego ze stanem klienta.

Tła aplikacji było prawdziwym właścicielem danych.

Interfejs użytkownika jedynie je konsumował.

To rozróżnienie jest kluczowe dla tego, dlaczego TanStack Query stał się tak interesującą alternatywą w nowoczesnych aplikacjach React.

Najpierw, czym dokładnie jest React Query?

Zanim przyjrzymy się, w jaki sposób zmniejsza potrzebę użycia Redux, warto wyjaśnić powszechny błąd zrozumienia.

TanStack Query, wcześniej nazywany React Query, nie jest zamiennikiem useState, Redux ani Zustand.

Jego głównym zadaniem jest zarządzanie stanem serwera — danymi pochodzącymi spoza aplikacji React, które należy pobrać, zapisać w cache’u, utrzymywać w synchronizacji, aktualizować i ostatecznie uznać za przestarzałe.

React Query nie polega tylko na wysłaniu żądania i umieszczeniu odpowiedzi w lokalnym stanie komponentu.

Dokumentacja samego TanStack opisuje tę bibliotekę przede wszystkim w kontekście pobierania, przechowywania w pamięci cache, synchronizacji oraz aktualizowania stanu serwera.

Pomyśl o danych w ten sposób:

Users
Projects
Messages
Notifications
Orders
Analytics

Twoja warstwa frontend nie posiada faktycznie żadnych z tych informacji.

To warstwa backend je posiada.

React Query pełni rolę pośrednika pomiędzy twoją interfejsem a tą warstwą backend, przejmując odpowiedzialność za cały cykl życia tych danych.

W centrum tego projektu znajduje się Query Cache.

Zgodnie z aktualną dokumentacją TanStack, QueryCache to warstwa przechowywania zapytań — przechowuje ich dane, metadane oraz status. QueryClient zarządza tym cache’em i udostępnia API, których aplikacja używa do odczytywania z niego danych, ich aktualizacji, unieważniania wpisów oraz innej interakcji z nim.

Dokładnie z tego powodu kilka niepowiązanych ze sobą części aplikacji może wysyłać tę samą zapytanie i otrzymywać spójne wyniki:

const { data } = useQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects
})

Jednak React Query robi o wiele więcej niż tylko przechowuje pojedynczą odpowiedź.

Zarządza on przechowywaniem w pamięci cache, eliminacją duplikatów zapytań, śledzeniem aktualności, ponownym pobieraniem danych w tle, logiką prób, zbieraniem niepotrzebnych danych, mutacjami oraz unieważnianiem danych, wszystko to w ramach tego samego systemu.

Naprzимер, po zaktualizowaniu projektu:

const queryClient = useQueryClient()
await updateProject(project)
queryClient.invalidateQueries({
  queryKey: ['projects']
})

Zamiast ręcznie instruować dziesięć oddzielnych komponentów, jak zaktualizować ich lokalną kopię projektu, wystarczy po prostu poinformować system zapytań:

">Dane serwera użyte w tym zapytaniu mogą już nie być dokładne."

Następnie pamięć cache zajmuje się ich ponowną weryfikacją.

To jest podstawowa idea leżąca u podstaw TanStack Query.

Nie ma on na celu zastąpienia Reduxa.

To nadaje stanowi serwera własny cykl życiowy.

Gdy zaczynasz traktować stan serwera jako coś fundamentalnie różnego od stanu klienta, staje się znacznie łatwiej zrozumieć, dlaczego aplikacja, która na pierwszy rzut oka wydaje się wymagać rozbudowanego sklepu Redux, w rzeczywistości może go wcale nie potrzebować.

Redux nigdy nie był problemem

Sprawiedliwie mówiąc, sam Redux nie zasługuje na żadne zarzuty.

Redux Toolkit nadal jest oficjalnie rekomendowanym podejściem do tworzenia aplikacji z użyciem Redux, a Redux nadal się sprawdza wtedy, gdy aplikacja naprawdę potrzebuje złożonego stanu po stronie klienta, przewidywalnych przejść między stanami, pipeline’ów middleware lub jednolitego modelu stanu.

Problemy pojawiają się, gdy programiści wrzucają absolutnie wszystko do jednego globalnego sklepu.

Wyobraź sobie aplikację wyglądającą w ten sposób:

{
  user: {},
  projects: [],
  teams: [],
  notifications: [],
  orders: [],
  products: [],
  analytics: {},
  theme: "dark",
  sidebarOpen: true
}

Na pierwszy rzut oka wydaje się, że to wszystko należy do aplikacji.

Jednak tak nie jest.

Spróbuj zadać jedno proste pytanie:

Kto tak naprawdę posiada te dane?

Czy lista projektów jest czymś, co kontroluje aplikacja React?

Nie do końca.

Prawdziwym właścicielem jest backend.

Czy inny użytkownik mógłby zmodyfikować zamówienie, podczas gdy karta przeglądarki pozostaje otwarta?

Oczywiście, że tak.

Czy powiadomienie mogłoby się pojawić bez żadnej akcji ze strony kodu React?

Tak, bez wątpienia.

Czy serwer mógłby jednostronnie cofnąć lub zmienić uprawnienia użytkownika?

Bез żadnych wątpliwości.

Zatem znaczna część tego „stanu aplikacji” w ogóle nie należy do frontendu.

To jest stan serwera.

A stan serwera niesie ze sobą zupełnie inny rodzaj wyzwań.

Należy go pobrać.

Należy go zapamiętać w buforze.

Należy ustalić, kiedy staje się przestarzały.

Należy go ponownie pobrać.

Należy utrzymywać jego synchronizację po dokonaniu modyfikacji.

Należy zarządzać wskaźnikami ładowania oraz stanami błędów.

Należy uwzględnić możliwość ponownych prób oraz przerwane połączenia sieciowe.

To właśnie ten zestaw problemów, który ma za zadanie rozwiązywać TanStack Query.

Zmiana architektury

Konwencjonalna konfiguracja oparta na Redux zazwyczaj przypomina taki schemat działania:

API
 ↓
Async action / thunk
 ↓
Reducer
 ↓
Redux Store
 ↓
Selector
 ↓
React Component

W przypadku wielu aplikacji opartych na API ten schemat może zostać przekształcony w coś takiego:

API
 ↓
TanStack Query
 ↓
Query Cache
 ↓
React Component

Różnica może wydawać się niewielka na papierze.

Nie jest tak.

Prawdziwa zmiana polega na tym, że nie musisz już ręcznie tworzyć całej infrastruktury niezbędnej do zarządzania stanem serwera.

Weźmy coś tak zwyczajnego jak listę projektów.

W Redux zazwyczaj zaczyna się od:

const initialState = {
  data: [],
  loading: false,
  error: null
}

Następnie dodaje się akcję asynchroniczną:

dispatch(fetchProjects())

Po czym następuje logika reduktora obejmująca:

pending
fulfilled
rejected

A na końcu selektor:

const projects = useSelector(
  state => state.projects.data
)

Teraz porównaj to z wersją TanStack Query:

const { data, isPending, error } = useQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects
})

To nie jest tylko zmniejszenie liczby linii kodu.

Sama zapytanie staje się abstrakcją otaczającą zasób serwera.

Nazwa klucza zapytania określa śledzony zasób.

Pamięć cache przechowuje wynik.

Zapytanie monitoruje swój własny stan.

I dowolna liczba komponentów może korzystać z tego samego wartości z cache’u.

QueryCache z TanStack Query istnieje specjalnie po to, aby przechowywać wyniki zapytań wraz z ich powiązanym stanem, a QueryClient dostarcza interfejs do pracy z tym cache’em.

Kache zapytań to w zasadzie globalne magazyn danej serwera

To może być najważniejszy koncepcja w tym temacie.

Bardzo wielu programistów słyszy takie stwierdzenie:

"React Query ma kache.

I zakłada, że oznacza to:

"Zatem jedynie przechowuje odpowiedzi API w kache.

Ale jest to coś więcej niż to.

Kache zapytań staje się w praktyce wspólnym, jedynym źródłem prawdy dotyczącym stanu serwera twojej aplikacji.

Wyobraź sobie trzy oddzielne komponenty:

Dashboard
   |
   +── ProjectList
   |
   +── ProjectSidebar
   |
   +── RecentProjects

Każdy z nich potrzebuje tych samych danych, oznaczonych kluczem:

['projects']

Nie ma potrzeby ręcznie przekazywać tych danych serwerowych do Redux, a następnie kazać wszystkim trzem komponentom czytać je ze sklepu.

Wystarczy wysyłać identyczną zapytanie tam, gdzie jest to potrzebne:

useQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects
})

TanStack Query zajmuje się dzieleniem się tymi danymi i ich cacheowaniem w tle.

Architektura, która powstaje, może wyglądać w ten sposób:

               React App
                  |
        ┌─────────┴─────────┐
        |                   |
   Client State        Server State
        |                   |
 Redux / Zustand       TanStack Query
        |                   |
     UI state         Query Cache

Nagle cały system staje się znacznie prostszy do zrozumienia.

Czy React Query może zastąpić Redux?

W niektórych aplikacjach tak, rzeczywiście może to zrobić.

Ale oto kluczowa niuans:

Nie zastępuje Redux, będąc lepszą wersją Redux.

Zamiast tego eliminuje potrzebę polegania na Redux dla stanu serwera od samego początku.

To zupełnie inna teza.

Dokumentacja samego TanStack Query wyraźnie przedstawia zarządzanie stanem serwera jako odrębny problem w porównaniu z zarządzaniem stanem klienta i wskazuje, że gdy stan serwera przenosi się do React Query, ilość stanu klienta, który nadal musi być zarządzany globalnie, może znacznie się zmniejszyć.

W tym momencie sytuacja staje się naprawdę interesująca z punktu widzenia architektury.

Możliwe jest użycie takiego podziału:

Client State
theme
sidebar
selectedTab
modal
editor
filters

Oprócz tego:

Server State
users
projects
orders
notifications
products
analytics

W tym przypadku wybór narzędzi staje się znacznie bardziej ukierunkowany:

Client State → Redux / Zustand / Context / React
Server State → TanStack Query

Zamiast oczekiwać, że jeden magazyn danych będzie wykonywał obie te funkcje jednocześnie.

Ale Redux nadal ma zadanie do wykonania

Rozważmy inny typ aplikacji, coś bliższego narzędziu do projektowania takiego jak Figma.

Możliwe jest przechowywanie takiego stanu:

{
  selectedLayer,
  activeTool,
  zoom,
  canvasMode,
  dragState,
  undoStack,
  redoStack
}

Żadna z tych rzeczy nie stanowi stanu serwera.

Frontend ma nad nią pełną kontrolę.

Zmienia się synchronicznie, w bezpośredniej reakcji na interakcję użytkownika.

Wiele elementów interfejsu użytkownika zależy od niej jednocześnie.

Czasami konieczne jest skoordynowanie złożonych przejść, gdy jeden element stanu wpływa na inny.

To właśnie taki scenariusz, w którym dedykowany menedżer stanu klienta okazuje się przydatny.

Dokumentacja TanStack Query podkreśla podobną kwestię: złożony stan sterowany interfejsem użytkownika, który nie ma nic wspólnego z serwerem, nadal może być powodem do użycia specjalistycznego narzędzia do zarządzania stanem klienta.

Zatem wniosek nie brzmi „usuń Redux wszędzie i zastąp go React Query”. To byłaby nadmierna korekta.

I jest jeszcze jeden ważny gracz: RTK Query

Warto wspomnieć o jeszcze jednej dodatkowej kwestii.

Redux Toolkit zawiera już RTK Query – warstwę do pobierania danych i cache’owania, zaprojektowaną specjalnie do pracy w aplikacjach Redux.

Może automatycznie generować hooki oraz zajmować się pobieraniem danych z endpointów, stanami ładowania i cache’owaniem, podobnie jak TanStack Query.

Zatem rzeczywista zmiana w tym ekosystemie nie polega po prostu na:

Redux → React Query

Wygląda to bardziej jak następująca ewolucja:

Manual API state in Redux
          ↓
Dedicated server-state solutions
          ↓
TanStack Query / RTK Query / Apollo / SWR

Cała branża stopniowo uświadamia sobie, że stan serwera i stan klienta to zasadniczo różne obowiązki, które wymagają odmiennych narzędzi.

Gdy już pojmiesz tę różnicę, cała architektura stanu staje się znacznie łatwiejsza do zrozumienia.

Zasada, której teraz używam

Przy decydowaniu o tym, czy dana informacja powinna znajdować się w globalnym stanie, kluczowe jest jedno pytanie:

Kto tak naprawdę posiada te dane?

Jeśli odpowiedź brzmi:

Tło systemu

prawie na pewno masz do czynienia ze stanem serwera.

Jeśli odpowiedź brzmi:

Interfejs użytkownika

prawie na pewno masz do czynienia ze stanem klienta.

To jedno pytanie zazwyczaj wskazuje na zupełnie inną architekturę w zależności od sytuacji.

Naprzykład:

Current user ────────── Server
Projects ────────────── Server
Orders ──────────────── Server
Notifications ───────── Server
Theme ───────────────── Client
Modal ───────────────── Client
Selected tab ────────── Client
Editor state ────────── Client

Gdy to w ten sposób przedstawisz, właściwa struktura staje się oczywista.

React Query nie niszczy Redux

Twierdzenie, że „React Query zastępuje Redux”, jest nieco mylące w tak sformułowanej postaci.

To, co naprawdę się zmienia, jest czymś bardziej subtelnym:

Rozwijający coraz lepiej potrafią określić, z jaką kategorią stanu faktycznie mają do czynienia.

Redux kiedyś był standardowym miejscem przechowywania wszystkiego, włączając w to odpowiedzi serwera.

Coraz częściej zespoły dokonują rozdzielenia:

Server state
        ↓
TanStack Query

z:

Client state
        ↓
Redux / Zustand / Context / React

Dla wielu nowoczesnych kodów React to samo rozdzielenie eliminuje zaskakująco dużą część złożoności, którą wcześniej przypisywano samemu Reduxowi.

Celem nie jest minimalizowanie liczby używanych bibliotek.

Celem jest przestanie ręcznego budowania infrastruktury dla problemów, które już mają solidne, specjalnie stworzone abstrakcje.

Zatem następnym razem, gdy natkniesz się na przeciążony fragment Reduxa pełen odpowiedzi API, flag ładowania, logiki unieważniania pamięci cache oraz działań ponownego pobierania danych, zadaj sobie pytanie:

Czy naprawdę potrzebujesz globalnego menedżera stanu do tego celu, czy po prostu ręcznie zaimplementowałeś React Query wewnątrz Reduxa?

Jak radzisz sobie z tym w swoich aplikacjach?

Czy obecnie przechowujesz dane serwerowe w Redux lub Zustand, polegasz na TanStack Query, czy stosujesz zupełnie inny podejście?

Literatura pokrewna

  • Zrozumienie custom hooks w React: ponowne użycie logiki bez wspólnego stanu — Dowiedz się, czym są custom hooks w React, jak wyodrębniają i udostępniają logikę związанą ze stanem pomiędzy komponentami oraz jakich błędów należy unikać podczas ich tworzenia.
  • Power Apps kontra React: porównanie kosztów długoterminowych i architektury — Ten artykuł analizuje ukryte koszty licencji, kompromisy architektoniczne oraz rzeczywistość zarządzania, które decydują o tym, który z tych narzędzi jest naprawdę tańszy w skali większej.
  • Przemyślenie o stanie React: Gdzie naprawdę powinny znajdować się twoje dane — Ten artykuł wyjaśnia, jak zmniejszyć błędy w React poprzez przenoszenie stanu do URL, DOM lub wartości pochodnych zamiast nadmiernego używania useState.