Zarządzanie stanem w React uproszczone: oddziel stan serwera od stanu klienta, przestrzegaj kilku zasad i szybko rozpoznawaj wczesne oznaki rosnącej złożoności.

Stan to dowolne dane, które mogą się zmieniać podczas działania aplikacji. To obejmuje to, co widzisz (modal jest otwarty), co edytujesz (szkic formularza) oraz dane, które pobierasz (lista projektów). Problem w tym, że wszystko to nazywa się "stan", mimo że zachowuje się bardzo różnie.
Większość zaplątanych aplikacji psuje się w ten sam sposób: zbyt wiele rodzajów stanu jest wymieszanych w tym samym miejscu. Komponent trzyma dane serwera, flagi UI, szkice formularzy i wartości pochodne, a potem próbuje je synchronizować efektami. Wkrótce nie potrafisz odpowiedzieć na proste pytania: "skąd pochodzi ta wartość?" albo "co ją aktualizuje?" bez przeszukiwania kilku plików.
Generowane aplikacje React wpadają w to szybciej, bo łatwo zaakceptować pierwszą działającą wersję. Dodajesz nowy ekran, kopiujesz wzorzec, łatasz błąd kolejnym useEffect, i nagle masz dwa źródła prawdy. Jeśli generator lub zespół zmienia kierunek w trakcie (lokalny stan tu, globalny store tam), kod zbiera wzorce zamiast budować jeden spójny.
Cel jest nudny: mniej rodzajów stanu i mniej miejsc do sprawdzenia. Gdy jest jedno oczywiste miejsce dla danych serwera i jedno dla stanu tylko UI, błędy stają się mniejsze, a zmiany przestają być ryzykowne.
"Keep it boring" oznacza trzymanie się kilku zasad:
Konkret: jeśli lista użytkowników pochodzi z backendu, traktuj ją jako server state i pobieraj tam, gdzie jest potrzebna. Jeśli selectedUserId istnieje tylko po to, by napędzać panel szczegółów, trzymaj go jako lokalny stan UI blisko tego panelu. Mieszanie tych dwóch rzeczy to początek narastającej złożoności.
Większość problemów ze stanem w React zaczyna się od jednego pomieszania: traktowania danych serwera jak stan UI. Oddziel je wcześnie, a zarządzanie stanem będzie spokojne, nawet gdy aplikacja urośnie.
Stan serwera należy do backendu: użytkownicy, zamówienia, zadania, uprawnienia, ceny, feature flagi. Może się zmieniać bez udziału twojej aplikacji (inna karta może je zaktualizować, admin edytuje, uruchamia się job, dane wygasają). Ponieważ jest współdzielony i zmienny, potrzebujesz fetchowania, cache'owania, refetchowania i obsługi błędów.
Stan klienta to to, czym interesuje się tylko twoje UI w danym momencie: który modal jest otwarty, która karta jest wybrana, przełącznik filtra, kolejność sortowania, zwinięty sidebar, szkic wyszukiwarki. Jeśli zamkniesz kartę, można to stracić.
Szybki test: "Czy mógłbym odświeżyć stronę i odbudować to z serwera?"
Jest też stan pochodny (derived), który ratuje Cię przed tworzeniem dodatkowego stanu. To wartość, którą możesz obliczyć z innych wartości, więc jej nie przechowujesz. Filtrowane listy, sumy, isFormValid i "pokaż pusty stan" zwykle należą tutaj.
Przykład: pobierasz listę projektów (server state). Wybrany filtr i flaga dialogu "Nowy projekt" to client state. Widoczna lista po filtrowaniu to stan pochodny. Jeśli przechowasz widoczną listę osobno, zacznie się rozjeżdżać i będziesz gonić błędy "dlaczego jest przestarzała?".
To rozdzielenie pomaga, gdy narzędzie takie jak Koder.ai generuje ekrany szybko: trzymaj dane backendu w jednej warstwie fetchującej, decyzje UI blisko komponentów i unikaj przechowywania wartości obliczonych.
Stan staje się bolesny, gdy jedna część danych ma dwóch właścicieli. Najszybszy sposób, by utrzymać prostotę, to zdecydować kto za co odpowiada i się tego trzymać.
Przykład: pobierasz listę użytkowników i pokazujesz szczegóły po wybraniu jednego. Powszechny błąd to trzymanie pełnego wybranego obiektu w stanie. Przechowuj selectedUserId zamiast tego. Trzymaj listę w cache'u serwera. Widok szczegółów wyszukuje użytkownika po ID, więc refetchy aktualizują UI bez dodatkowego kodu sync.
W generowanych aplikacjach React łatwo zaakceptować "pomocny" wygenerowany stan, który duplikuje dane serwera. Gdy widzisz kod robiący fetch -> setState -> edytuj -> refetch, zatrzymaj się. To często znak, że budujesz drugą bazę danych w przeglądarce.
Server state to wszystko, co żyje na backendzie: listy, strony szczegółów, wyniki wyszukiwania, uprawnienia, liczniki. Nudne podejście to wybrać jedno narzędzie do tego i się go trzymać. W wielu aplikacjach React TanStack Query wystarcza.
Cel jest prosty: komponenty proszą o dane, pokazują loading i error, i nie przejmują się ile fetchów dzieje się pod spodem. To ważne w generowanych aplikacjach, bo małe niespójności mnożą się szybko, gdy dodaje się nowe ekrany.
Traktuj query keys jak system nazewnictwa, nie dodatek. Utrzymuj je spójne: stabilne tablice kluczy, uwzględniaj tylko wejścia zmieniające wynik (filtry, strona, sort), i preferuj kilka przewidywalnych kształtów zamiast wielu jednorazowych. Wiele zespołów umieszcza budowanie kluczy w małych helperach, żeby każdy ekran używał tych samych reguł.
Do zapisów używaj mutation z wyraźną obsługą sukcesu. Mutacja powinna odpowiadać na dwa pytania: co się zmieniło i co UI powinno zrobić dalej?
Przykład: tworzysz nowe zadanie. Po sukcesie albo unieważnij zapytanie listy zadań (żeby się przeładowało raz), albo zrób ukierunkowaną aktualizację cache'u (dodaj nowe zadanie do zbuforowanej listy). Wybierz jedną metodę na funkcję i trzymaj się jej.
Jeśli czujesz pokusę dodawać refetchy w wielu miejscach "na wszelki wypadek", wybierz jedną nudną akcję zamiast tego:
Client state to rzeczy należące do przeglądarki: flaga otwartego sidebaru, wybrany wiersz, tekst filtra, szkic przed zapisem. Trzymaj go blisko miejsca użycia, a zwykle pozostanie on do opanowania.
Zacznij od małego: useState w najbliższym komponencie. Przy generowaniu ekranów (np. z Koder.ai) kuszące jest wrzucenie wszystkiego do globalnego store'a "na wypadek". Tak kończy się store, którego nikt nie rozumie.
Przenieś stan w górę tylko wtedy, gdy potrafisz nazwać problem współdzielenia.
Przykład: tabela z panelem szczegółów może trzymać selectedRowId w komponencie tabeli. Jeśli toolbar w innej części strony też go potrzebuje, podnieś go do komponentu strony. Jeśli osobna trasa (np. bulk edit) też go potrzebuje, wtedy mały store ma sens.
Jeśli używasz store'a (Zustand lub podobny), trzymaj go skoncentrowanego na jednym zadaniu. Przechowuj "co" (wybrane ID, filtry), nie "wyniki" (posortowane listy), które możesz wyprowadzić.
Gdy store zaczyna rosnąć, zapytaj: czy to wciąż jedna funkcja? Jeśli odpowiedź brzmi "tak na pół", podziel go teraz, zanim kolejna funkcja zamieni go w kulę stanu, której boisz się dotykać.
Błędy w formularzach często wynikają z mieszania trzech rzeczy: tego, co użytkownik wpisuje, tego, co serwer zapisał, i tego, co UI pokazuje.
Dla nudnego zarządzania stanem traktuj formularz jako client state do momentu wysłania. Dane serwera to ostatnia zapisana wersja. Formularz to szkic. Nie edytuj obiektu serwera „na miejscu”. Skopiuj wartości do szkicu, pozwól użytkownikowi swobodnie zmieniać, a potem zatwierdź i refetchuj (lub zaktualizuj cache) po sukcesie.
Zdecyduj wcześnie, co powinno przetrwać nawigację użytkownika. Ta jedna decyzja zapobiega wielu niespodziankom. Na przykład inline edit i otwarte dropdowny zwykle resetują się, podczas gdy długi szkic kreatora lub nie wysłana wiadomość mogą być zapisywane. Trzymaj trwałość po reload tylko gdy użytkownicy jej oczekują (np. koszyk zakupowy).
Trzymaj reguły walidacji w jednym miejscu. Jeśli rozrzucisz reguły po inputach, handlerach submit i helperach, skończysz z niespójnymi błędami. Preferuj jedną schemę (albo jedną funkcję validate()), a UI niech decyduje, kiedy pokazywać błędy (on change, on blur lub on submit).
Przykład: generujesz ekran Edytuj Profil w Koder.ai. Załaduj zapisany profil jako server state. Stwórz szkic lokalny dla pól formularza. Pokaż "niesave'owane zmiany" porównując szkic z wersją zapisaną. Jeśli użytkownik anuluje, porzuć szkic i pokaż wersję z serwera. Jeśli zapisze, wyślij szkic, a potem zastąp wersję zapisaną odpowiedzią z serwera.
W miarę jak generowana aplikacja React rośnie, często te same dane pojawiają się w trzech miejscach: stanie komponentu, globalnym store i cache'u. Naprawa zwykle nie wymaga nowej biblioteki. To wybór jednego domu dla każdego kawałka stanu.
Przepływ porządkowy, który działa w większości aplikacji:
filteredUsers, jeśli możesz go obliczyć z users + filter. Preferuj selectedUserId zamiast zdublowanego selectedUser.Przykład: wygenerowana przez Koder.ai aplikacja CRUD często zaczyna się od useEffect fetch + globalna kopia listy. Po scentralizowaniu server state lista pochodzi z jednego zapytania, a "odśwież" staje się invalidacją zamiast ręcznej synchronizacji.
Dla nazewnictwa trzymaj je spójne i nudne:
users.list, users.detail(id)ui.isCreateModalOpen, filters.userSearchopenCreateModal(), setUserSearch(value)users.create, users.update, users.deleteCel: jedno źródło prawdy dla każdej rzeczy i wyraźne granice między stanem serwera a stanem klienta.
Problemy ze stanem zaczynają się mało, potem pewnego dnia zmieniasz pole i trzy części UI nie zgadzają się co do "prawdziwej" wartości.
Najjaśniejszym sygnałem ostrzegawczym są zdublowane dane: ten sam użytkownik lub koszyk żyje w komponencie, globalnym store i cache'u zapytań. Każda kopia aktualizuje się w innym czasie i dodajesz więcej kodu tylko po to, by je zrównać.
Inny sygnał to kod synchronizujący: efekty, które przesyłają stan tam i z powrotem. Wzorce typu "gdy query się zmienia, aktualizuj store" i "gdy store się zmienia, refetch" działają, dopóki przypadek brzegowy nie wywoła stale'ujących wartości lub pętli.
Kilka szybkich czerwonych flag:
needsRefresh, didInit, isSaving, których nikt nie usuwa.Przykład: generujesz dashboard w Koder.ai i dodajesz modal Edit Profile. Jeśli dane profilu są w query cache, skopiowane do globalnego store i zdublowane w lokalnym stanie formularza, masz teraz trzy źródła prawdy. Przy pierwszym dodaniu background refetch lub optimistic update pokażą niespójności.
Gdy widzisz te znaki, nudny ruch to wybrać jednego właściciela dla każdego kawałka danych i usunąć mirror'y.
Przechowywanie rzeczy "na zapas" to jedna z najszybszych dróg do bolesnego stanu, szczególnie w generowanych aplikacjach.
Kopiowanie odpowiedzi API do globalnego store'a to częsta pułapka. Jeśli dane pochodzą z serwera (listy, szczegóły, profil użytkownika), nie kopiuj ich domyślnie do store'a klienta. Wybierz jedno miejsce dla danych serwera (zazwyczaj query cache). Używaj client store dla wartości UI, o których serwer nie wie.
Przechowywanie wartości pochodnych to kolejna pułapka. Liczniki, filtrowane listy, sumy, canSubmit i isEmpty zwykle powinny być obliczane z wejść. Jeśli wydajność stanie się realnym problemem, memoizuj później, ale nie zaczynaj od przechowywania wyniku.
Pojedynczy mega-store na wszystko (auth, modale, toasty, filtry, szkice, flagi onboardingowe) staje się śmietnikiem. Dziel według granic funkcji. Jeśli stan jest używany tylko przez jeden ekran, trzymaj go lokalnie.
Context jest świetny dla wartości stabilnych (theme, current user id, locale). Dla szybko zmieniających się wartości może powodować szerokie rerendery. Używaj Context do przekazywania konfiguracji, a stan często zmieniający się trzymaj w stanie komponentu (lub małym store).
Na koniec: unikaj niespójnego nazewnictwa. Prawie identyczne query keys i pola store'a tworzą subtelne duplikacje. Wybierz prosty standard i się go trzymaj.
Gdy masz ochotę dodać "jeszcze jedną" zmienną stanu, zrób szybki test własności.
Po pierwsze: czy potrafisz wskazać jedno miejsce, gdzie zachodzi fetching i cache'owanie serwera (jedno narzędzie, jeden zestaw query keys)? Jeśli te same dane są fetchowane w wielu komponentach i też kopiowane do store'a, już płacisz odsetki.
Po drugie: czy ta wartość jest potrzebna tylko w jednym ekranie (np. "panel filtra otwarty")? Jeśli tak, nie powinna być globalna.
Po trzecie: czy możesz przechowywać ID zamiast duplikować obiekt? Przechowuj selectedUserId i odczytuj użytkownika z cache lub listy.
Po czwarte: czy to wartość pochodna? Jeśli da się ją obliczyć z istniejącego stanu, nie przechowuj jej.
Wreszcie, zrób test trasowania na minutę. Jeśli kolega nie potrafi odpowiedzieć "skąd ta wartość pochodzi?" (prop, lokalny stan, cache serwera, URL, store) w mniej niż minutę, napraw własność zanim dodasz więcej stanu.
Wyobraź sobie wygenerowane admin UI (np. z promptu w Koder.ai) z trzema ekranami: lista klientów, strona szczegółów klienta i formularz edycji.
Stan pozostaje spokojny, gdy ma oczywiste domy:
Strony listy i szczegółów czytają server state z query cache. Gdy zapisujesz, nie przechowujesz klientów ponownie w globalnym store. Wysyłasz mutację, a potem pozwalasz cache'owi odświeżyć się lub aktualizujesz go.
Dla ekranu edycji trzymaj szkic lokalnie. Inicjalizuj go z pobranego klienta, ale traktuj jako oddzielny, gdy użytkownik zaczyna pisać. Dzięki temu widok szczegółów może się bezpiecznie odświeżyć bez nadpisywania niedokończonych zmian.
UI optymistyczne to miejsce, gdzie zespoły często duplikują wszystko. Zazwyczaj nie trzeba tego robić.
Gdy użytkownik klika Zapisz, zaktualizuj tylko zbuforowany rekord klienta i odpowiadający element listy, a potem wycofaj zmianę, jeśli żądanie się nie powiedzie. Trzymaj szkic w formularzu dopóki zapis nie powiedzie się. Jeśli nie uda się, pokaż błąd i zostaw szkic, żeby użytkownik mógł spróbować ponownie.
Załóżmy, że dodajesz bulk edit i też potrzebuje wybranych wierszy. Zanim stworzysz nowy store, zapytaj: czy ten stan powinien przetrwać nawigację i reload?
Generowane ekrany mogą mnożyć się szybko i to świetnie, dopóki każdy nowy ekran nie wnosi własnych decyzji odnośnie stanu.
Zapisz krótką notkę zespołową w repo: co liczy się jako server state, co jako client state i które narzędzie za co odpowiada. Trzymaj to na tyle krótkie, by ludzie rzeczywiście tego przestrzegali.
Dodaj mały zwyczaj w PR: oznacz każdy nowy kawałek stanu jako server lub client. Jeśli to server state, zapytaj "gdzie się ładuje, jak jest cache'owane i co to unieważnia?" Jeśli to client state, zapytaj "kto za to odpowiada i kiedy się resetuje?"
Jeśli używasz Koder.ai (koder.ai), Planning Mode może pomóc uzgodnić granice stanu zanim wygenerujesz nowe ekrany. Migawka i rollback dają bezpieczny sposób na eksperymentowanie, gdy refaktor stanu idzie nie tak.
Wybierz jedną funkcję (np. edycja profilu), zastosuj reguły od początku do końca i niech to będzie przykład, który wszyscy kopiują.
Zacznij od oznaczenia każdego kawałka stanu jako server, client (UI) lub derived.
isValid).Gdy je oznaczysz, upewnij się, że każdy element ma jedno oczywiste miejsce odpowiedzialności (query cache, lokalny stan komponentu, URL lub mały store).
Użyj prostego testu: „Czy mogę odświeżyć stronę i zbudować to ponownie z serwera?”
Przykład: lista projektów to server state; to client state.
Bo tworzy to dwa źródła prawdy.
Jeśli pobierasz users, a potem kopiujesz je do useState lub globalnego store'a, musisz je teraz utrzymywać w synchronizacji przy:
Zasada: i twórz lokalny stan tylko dla UI lub szkiców.
Przechowuj pochodne wartości tylko wtedy, gdy naprawdę nie da się ich tanio obliczyć.
Zwykle obliczasz je z istniejących wejść:
visibleUsers = users.filter(...)total = items.reduce(...)canSubmit = isValid && !isSavingJeśli problemem jest wydajność (zmierzone), preferuj lub lepsze struktury danych, zanim dodasz więcej przechowywanego stanu, który może się zdezaktualizować.
Domyślnie: użyj narzędzia do server-state (często TanStack Query), aby komponenty mogły po prostu „poprosić o dane” i obsłużyć loading/error.
Praktyczne podstawy:
Trzymaj to lokalnie dopóki nie potrafisz nazwać rzeczywistej potrzeby współdzielenia.
Reguła promocji:
Dzięki temu globalny store nie stanie się składowiskiem losowych flag UI.
Przechowuj ID i małe flagi, nie całe obiekty serwera.
Przykład:
selectedUserIdselectedUser (skopiowany obiekt)Następnie renderuj szczegóły, odczytując użytkownika z cache lub listy. Dzięki temu background refetch i aktualizacje będą działać poprawnie bez dodatkowych efektów synchronizacji.
Traktuj formularz jako szkic (client state) dopóki nie zatwierdzisz.
Wzorzec praktyczny:
To zapobiega przypadkowemu edytowaniu danych serwera „in place” i walce z refetchami.
Typowe czerwone flagi:
needsRefresh, didInit, isSaving, które się kumulują.Generowane ekrany szybko popadają w mieszane wzorce. Proste zabezpieczenie: ustal własność stanów:
Jeśli używasz Koder.ai, skorzystaj z Planning Mode, aby ustalić granice stanu przed generowaniem nowych ekranów i używaj snapshotów/rollbacku przy eksperymentach.
selectedRowIduseMemoUnikaj rozsypywania refetch() po kodzie „na wszelki wypadek.”
Naprawa zwykle nie wymaga nowej biblioteki — trzeba usunąć kopie i wybrać jednego właściciela dla każdej wartości.