Strona główna / Artykuły

Przemyślenie stanu w React: gdzie powinny faktycznie znajdować się twoje dane

Mniej błędów w React dzięki mniejszemu użyciu useState: przenieś stan do adresu URL, DOM-u lub wartości pochodnych oraz zdecyduj, kto jest odpowiedzialny za każdą część, zanim zaczniesz korzystać z mechanizmów zarządzania stanem.

Wydawca: REACTAPP.TOP17 min czytania3931 słów

Źródła: Stop Adding React State: It’s Creating Bugs You Don’t Need

Przedstawiciel sprzedaży w rozwijającej się firmie typu SaaS spędza znaczną część poranka na przeglądaniu bazy danych klientów zawierającej dziesięć tysięcy kont.

Po dokładnym filtrowaniu wyodrębnia klientów korporacyjnych z Europy, których subskrypcje nie zostały odnowione w ciągu ostatnich dziewięćdziesięciu dni.

W rezultacie powstaje dokładna lista trzydziestu kont wymagających natychmiastowej uwagi.

Bierze adres URL bezpośrednio z przeglądarki i wysyła go przez Slack do swojego kierownika regionalnego wraz z notatką:

">Oto konta o najwyższym priorytecie na dziś rano."

Kierownik otwiera link.

Strona się załadowuje.

Jednak zamiast sfiltrowanej listy pokazuje standardowy widok: wszystkie dziesięć tysięcy kont, niefiltrowane i niesortowane.

Całe to staranne filtrowanie zniknęło, ponieważ inżynier, który stworzył tę stronę, przechowywał każdy filtr, pole sortowania oraz wartość paginacji w lokalnym stanie komponentu za pomocą useState.

Scenariusz ten ilustruje wzorzec, który stale pojawia się w kodach napisanych w React.

Mamy tendencję do korzystania ze stanu po prostu dlatego, że React ułatwia jego tworzenie.

Potrzebna jest wartość filtru?

useState.

Należy śledzić, który zakładka jest aktywna?

useState.

Potrzebny jest licznik?

useState.

Należy przechowywać pole formularza?

useState.

Należy śledzić różne etapy wywołania API?

To kolejne trzy wywołania useState.

Niedługo potem jeden komponent musi zarządzać dziesiątkami wartości, które tak naprawdę od początku nie musiały znajdować się w stanie React.

Sam stan nie jest tutaj wrogiem. Każde złożone aplikacje React potrzebują go.

Prawdziwym problemem jest umieszczanie stanu w miejscu, gdzie nie powinien być.

Każda dodatkowa zmienna stanu to kolejna rzecz, którą aplikacja musi synchronizować z całym resztą.

Jeśli tego nie kontrolować, może to prowadzić do różnych problemów, między innymi:

  • Dodatkowa praca potrzebna do utrzymywania zgodności duplikatów wartości
  • Dane, które stają się przestarzałe w jakimś cache’u
  • Wartości wyświetlane na ekranie, które już nie odpowiadają rzeczywistości
  • Renderowanie, które następuje mimo braku faktycznych zmian
  • Błędy czasowe, gdy dwie aktualizacje kolidują w nieprzewidywalny sposób
  • Efekty, które stają się coraz bardziej skomplikowane w próbach utrzymania spójności
  • Błędy, których źródło jest trudniejsze do zlokalizowania
  • Strona, która zapomina o wszystkim zaraz po ponownym załadowaniu
  • Ekrany, do których nikt nie może stworzyć linku ani je zapisać na później
  • Kombinacje flag, które powodują stan interfejsu, który nie powinien istnieć
  • Pisanie doskonałego kodu w React nie polega tak naprawdę na opanowaniu złożonej logiki stanu.

    Często chodzi o to, by rozpoznać, kiedy w ogóle nie należy tworzyć danego stanu.

    1. URL to najlepszy menedżer stanu w Twojej aplikacji

    Wyobraź sobie typowy ekran aplikacji pełen danych.

    Prawdopodobnie musi on śledzić:

    • Szukanie
    • Filtrowanie
    • Sortowanie
    • Paginacja
    • Aktywne karty
    • Wybrane kategorie
    • Odległości czasowe

    Zwykle stosowanym podejściem jest umieszczenie tego wszystkiego w stanie komponentu.

    function ProductList() {
      const [activeCategory, setActiveCategory] = useState('all');
      const [currentPage, setCurrentPage] = useState(1);
      const [searchQuery, setSearchQuery] = useState('');
    
      // Fetch and filter data...
    }
    

    Na pierwszy rzut oka wydaje się to w pełni w porządku.

    Następnie użytkownik naciska „Odśwież”.

    Każda wartość wraca do swojej domyślnej.

    Lub udostępniają link koleżeńskiemu.

    Kolega ten trafia na zupełnie inny widok.

    Lub klikają przycisk „Wstecz” w przeglądarce.

    Filtracja, którą ustawili, może nie zostać przywrócona, ponieważ stan React nie ma wbudowanego połączenia z historią nawigacji przeglądarki.

    Sam useState nie jest tu winny.

    Prawdziwym problemem jest to, że URL już istnieje jako mechanizm do reprezentowania stanu nawigacyjnego, a jest on całkowicie pomijany.

    Zamiast pisać:

    const [currentPage, setCurrentPage] = useState(1);
    const [searchQuery, setSearchQuery] = useState('');
    const [category, setCategory] = useState('all');
    

    można bezpośrednio zakodować te same informacje w URL.

    Coś w rodzaju:

    /products?category=enterprise&page=3&query=react
    

    Używając biblioteki routingu takiej jak Next.js, React Router lub TanStack Router, można odczytać te parametry bezpośrednio z URL:

    import { useSearchParams } from 'next/navigation';
    
    function ProductList() {
      const searchParams = useSearchParams();  const category = searchParams.get('category') || 'all';
      const page = Number(searchParams.get('page')) || 1;
      const query = searchParams.get('query') || '';  // Fetch data using the URL state...
    }
    

    To niewielkie zmiany drastycznie wpływają na cykl życia tego stanu.

    Odświeżyć stronę?

    Filtrzy pozostają nienaruszone.

    Podzielić się linkiem?

    Każdy, kto go otworzy, zobaczy dokładnie tę samą widok.

    Zapisać jako zakładkę?

    Później zostanie naładowana w tym samym stanie.

    Kliknąć „Wstecz”?

    Brauzer poprawnie przechodzi między poprzednimi stanami.

    To znacząca zaleta pod względem architektury.

    Stan URL daje więcej niż tylko możliwość przechowywania danych

    Przetrwanie odświeżenia strony to tylko część korzyści.

    Większą zaletą jest to, że URL staje się przenośnym zrzutem obrazu aktualnego stanu aplikacji, który można podzielić z kimkolwiek.

    Weźmy jako przykład panel sterowania obsługą klienta:

    /support/tickets?status=open&priority=high&team=billing
    

    Jeden menedżer może natychmiast wysłać ten dokładny link innemu menedżerowi.

    Panel sterowania skupiony na sprzedaży może wyglądać tak:

    /accounts?region=europe&plan=enterprise&renewal=90
    

    Narzędzie analityczne może używać czegoś w rodzaju:

    /analytics?range=30d&metric=revenue&groupBy=region
    

    Żaden z tych nie jest dowolnym łańcuchem zapytania.

    Każdy z nich to głęboki link wskazujący na konkretny, możliwy do odtworzenia stan aplikacji.

    Dokładnie do tego służą adresy URL.

    Kiedy stan w URL ma sens

    Parametry URL doskonale nadają się do przechowywania takich informacji jak:

    • To, co użytkownik wpisał do pola wyszukiwania
    • Aktywne wybory filtrów
    • Kolumny i kierunek sortowania danych
    • Strona wyników, która jest obecnie pokazywana
    • Bieżąco wybrana karta
    • Wybrany zakres dat
    • Obecnie otwarty zapisany widok
    • Ustawienia raportu wygenerowanego automatycznie
    • Dostosowana konfiguracja na panelu sterowania
    • Każdy stan interfejsu, który użytkownik może chcieć przekazać komuś innemu

    Mimo to nie wszystko pasuje do tego formatu.

    Haseła oczywiście nie powinny znajdować się w adresie URL.

    Również stan tymczasowej animacji modalnej nie powinien być przechowywany.

    Ogólnie mówiąc, nie powinien być to też duży, głęboko zagnieżdżony obiekt.

    Pytanie przewodnie jest proste:

    Zadaj sobie pytanie, czy użytkownik miałby uzasadnione oczekiwania, że ta widok będzie możliwy do udostępnienia, zaznaczania jako ulubiony lub przywrócenia — jeśli tak, URL jest prawdopodobnie odpowiednim miejscem na przechowywanie tego stanu.

    2. Przestań przechowywać wartości, które można łatwo obliczyć

    Szybkim sposobem na zmniejszenie ilości stanu w React jest zaprzestanie przechowywania wartości, które w rzeczywistości pochodzą z innych wartości.

    Weźmy ten przykład:

    const [items, setItems] = useState([]);
    const [itemCount, setItemCount] = useState(0);
    

    A gdzie indziej w komponencie:

    setItems(newItems);
    setItemCount(newItems.length);
    

    Masz teraz dwie oddzielne zmienne stanu opisujące tę samą rzecz.

    Jeśli items zostanie zaktualizowany, ale itemCount nie pójdzie w jego ślady, twoja interfejs pokaże nieprawidłowe dane.

    Stworzyłeś problem z synchronizacją, który w ogóle nie musiał istnieć.

    Czystsza wersja wygląda tak:

    const [items, setItems] = useState([]);
    
    const itemCount = items.length;
    

    Teraz istnieje jedyny źródło prawdy.

    Liczba jest obliczana za każdym razem, gdy komponent jest renderowany.

    Ta sama logika ma zastosowanie przy filtrowaniu kolekcji.

    Zamiast pisać:

    const [items, setItems] = useState([]);
    const [activeItems, setActiveItems] = useState([]);
    

    zwykle można po prostu napisać:

    const activeItems = items.filter(item => item.active);
    

    Lub:

    const totalPrice = items.reduce(
      (total, item) => total + item.price,
      0
    );
    

    Lub:

    const hasErrors = fields.some(field => field.error);
    

    Lub:

    const completedCount = todos.filter(todo => todo.completed).length;
    

    Pytanie, które warto zadać, to nie jest:

    ">Czy mogę to technicznie przechowywać w stanie?"

    Oczywiście, że możesz.

    To, o co powinieneś zapytać, to:

    ">Czy ta wartość ma własny, odrębny cykl życia?"

    Jeśli nie, prawdopodobnie nie powinna być częścią stanu.

    3. Przestań łączyć każde pole formularza ze stanem

    Kolejnym nawykiem, którego warto się pozbyć, jest traktowanie każdego pola formularza jako odrębnej zmiennej stanu.

    Niewielki formularz zbudowany w ten sposób może wyglądać tak:

    function ContactForm() {
      const [firstName, setFirstName] = useState('');
      const [lastName, setLastName] = useState('');
      const [email, setEmail] = useState('');
      const [message, setMessage] = useState('');
    
      const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();    submitData({
          firstName,
          lastName,
          email,
          message
        });
      };  return (
        <form onSubmit={handleSubmit}>
          <input
            value={firstName}
            onChange={e => setFirstName(e.target.value)}
          />      <input
            value={lastName}
            onChange={e => setLastName(e.target.value)}
          />      <input
            value={email}
            onChange={e => setEmail(e.target.value)}
          />      <textarea
            value={message}
            onChange={e => setMessage(e.target.value)}
          />      <button type="submit">Send</button>
        </form>
      );
    }
    

    Technicznie nie ma tu nic błędnego.

    Wprowadzenia kontrolowane są przydatne wtedy, gdy aplikacja naprawdę musi reagować na to, co wpisuje użytkownik. Oto kilka sytuacji, w których jest to istotne:

    • Weryfikacja poprawności pola w trakcie wpisywania
    • Oferowanie sugestii w stylu autodopasowywania
    • Modyfikacja formatu tekstu w czasie rzeczywistym
    • Liczenie pozostałych znaków
    • Pokazywanie lub ukrywanie innych pól w zależności od wprowadzonych danych
    • Ocena siły hasła
    • Łączenie zachowania jednego pola z wartością innego

    Jednak często aplikacja nie ma powodu, by przejmować się tym, co wpisał użytkownik, dopóki nie naciśnie Submit.

    W takich przypadkach React nie musi śledzić każdej litery w miarę jej wpisywania.

    Brauzer już zajmuje się tym zadaniem.

    Pozwól brzegownikowi zająć się tym

    Rodzinne formularze HTML zarządzają wartościami wprowadzanymi od dziesięcioleci.

    Możesz skorzystać z tego wbudowanego zachowania przy użyciu FormData:

    function ContactForm() {
      const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
    
        const formData = new FormData(e.currentTarget);    const payload = {
          firstName: formData.get('firstName') as string,
          lastName: formData.get('lastName') as string,
          email: formData.get('email') as string,
          message: formData.get('message') as string,
        };    submitData(payload);
      };  return (
        <form onSubmit={handleSubmit}>
          <input name="firstName" required />
          <input name="lastName" required />
          <input name="email" type="email" required />
          <textarea name="message" required />      <button type="submit">Send</button>
        </form>
      );
    }
    

    Dzięki temu wzorcowi React nie potrzebuje już osobnego pola stanu dla każdego pola.

    Brauzer przechowuje to, co wpisał użytkownik.

    React po prostu odczytuje te wartości po wysłaniu formularza.

    Jest to szczególnie przydatne w przypadku dużych formularzy.

    Pomyśl o procesie onboardingu dla przedsiębiorstwa z 30 polami.

    Czy naprawdę chciałbyś mieć 30 oddzielnych zmiennych stanu tylko po to, by przechowywać to, co ktoś wpisał?

    Prawdopodobnie nie.

    Wejścia kontrolowane i niekontrolowane mogą współistnieć

    To wcale nie oznacza, że komponenty kontrolowane są błędem.

    One nie są.

    Istnieje wiele sytuacji, w których kontrola danych wejściowych jest dokładnie tym, co należy zrobić.

    Cel polega po prostu na porzuceniu założenia, że React musi domyślnie kontrolować wartość każdego pola.

    Jeśli twoja aplikacja nie potrzebuje tych danych aż do wysłania formularza, często wystarczy pozwolić na ich przechowywanie w standardowym formularzu.

    Rozróżnienie to może znacznie uproszczyć duże formularze.

    4. Dlaczego flagi logiczne zawodzą

    Kolejny częsty problem pojawia się ciągle w rzeczywistych bazach kodu.

    Rozważmy typowe wezwanie API.

    Powszechnym rozwiązaniem jest śledzenie tego stanu za pomocą trzech oddzielnych flag logicznych:

    const [isLoading, setIsLoading] = useState(false);
    const [isSuccess, setIsSuccess] = useState(false);
    const [isError, setIsError] = useState(false);
    

    Na początku wszystkie trzy są fałszywe:

    loading = false
    success = false
    error = false
    

    Gdy tylko rozpoczyna się żądanie:

    loading = true
    

    Gdy zostanie ono pomyślnie zakończone:

    loading = false
    success = true
    

    Na pierwszy rzut oka wydaje się to rozsądne.

    Ale załóżmy, że gałąź obsługująca błędy nie zdoła sformatować wartości isLoading.

    Możliwe jest, że otrzymasz:

    loading = true
    error = true
    

    Albo nieodpowiednio zaprojektowany mechanizm ponawiania prób może spowodować, że otrzymasz:

    success = true
    error = true
    

    W tym momencie komponent znajduje się w stanie, który logicznie nie ma sensu.

    Czy powinien wyświetlać komunikat o sukcesie?

    Czy powinien wyświetlać informację o błędzie?

    Czy nadal powinien pokazywać ikonkę spinera?

    Z technicznego punktu widzenia wszystkie trzy wartości logiczne mogą jednocześnie uzasadniać odpowiedź „tak”.

    To właśnie ta niejednoznaczność stanowi błąd.

    Lepiej traktuj to jako maszynę stanów skończonych

    Czystsze podejście:

    type RequestStatus =
      | 'idle'
      | 'loading'
      | 'success'
      | 'error';
    
    const [status, setStatus] =
      useState<RequestStatus>('idle');
    

    Teraz każdy ważny stan jest jasno określony i wzajemnie wykluczający się.

    Zapytanie może mieć tylko jeden z następujących stanów:

    idle
    loading
    success
    error
    

    Nie ma możliwości przypadkowego przedstawienia jednocześnie stanów „ładowanie” i „sukces”.

    Renderyzacja staje się również znacznie prostsza do zrozumienia:

    if (status === 'loading') {
      return <Spinner />;
    }
    
    if (status === 'error') {
      return <ErrorMessage />;
    }if (status === 'success') {
      return <SuccessBanner />;
    }return <DefaultForm />;
    

    To nie jest tylko dodatkową zaletą TypeScript.

    Odbija to szerszą zasadę projektowania:

    Gdy dwa elementy stanu nigdy nie mogą być prawdziwe niezależnie od siebie, nie powinny być modelowane jako dwie oddzielne zmienne.

    5. Unikaj używania efektów do synchronizacji stanu

    To kolejny przypadek, gdy dodatkowe zmienne stanu generują zbędny, unikniony kod.

    Weźmy taką konfigurację:

    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const [fullName, setFullName] = useState('');
    

    W połączeniu z:

    useEffect(() => {
      setFullName(`${firstName} ${lastName}`);
    }, [firstName, lastName]);
    

    To, co tutaj stworzyłeś, to mechanizm synchronizacji wartości, która od początku nie potrzebowała własnego stanu.

    Prostsze rozwiązanie:

    const fullName = `${firstName} ${lastName}`;
    

    Zmiana może wydawać się drobna, ale jej wpływ na architekturę jest znaczący.

    Oryginalna wersja wymaga:

    • Trzech oddzielnych zmiennych stanu
  • Efekt służący do utrzymania ich wyrównania
  • Niewidzialna relacja synchronizacji między wartościami
  • Dodatkowy cykl renderowania wywołany przez efekt
  • Tablica zależności, która musi pozostać dokładna
  • Zrewidowana wersja wymaga jedynie:

    • Dwóch rzeczywistych wartości
    • Jednej wartości wyprowadzonej z nich
    • Żadnego efektu
    • Żadnej logiki synchronizacji do utrzymania

    Dobrym nawykiem podczas przeglądania komponentów React jest zadawanie pytania:

    "Dlaczego ta wartość musi znajdować się poza obliczeniami renderowania?"

    Jeśli uczciwa odpowiedź brzmi „aby być zsynchronizowana z czymś innym”, to zazwyczaj oznacza, że ta wartość w ogóle nie powinna być stanem.

    6. Trzy pytania do zadań przed dodaniem stanu

    Zanim napiszesz tę linię:

    const [value, setValue] = useState(...);
    

    najpierw sprawdź trzy kryteria.

    Pytanie 1: Czy można to obliczyć podczas renderowania?

    Jeśli dane potrzebne do utworzenia tej wartości już istnieją, nie ma powodu, by przechowywać jej kopię.

    Zamiast:

    const [itemCount, setItemCount] = useState(0);
    

    pisać:

    const itemCount = items.length;
    

    Zamiast:

    const [hasErrors, setHasErrors] = useState(false);
    

    pisać:

    const hasErrors = fields.some(field => field.error);
    

    Unikaj przechowywania stanu dla wszystkiego, co można wyliczyć na miejscu.

    Pytanie 2: Czy powinno to przetrwać ponowne załadowanie strony?

    Jeśli tak, rozważ, czy URL jest odpowiednim nośnikiem tych danych.

    Typowe przykłady:

    • Słowa kluczowe
    • Stan paginacji
    • Aktywne filtry
    • Kolejność sortowania
    • Wybrane karty
    • Odległości czasowe

    Zadaj sobie pytanie: jeśli zostanie udostępniony link do tej strony, czy odbiorca powinien trafić dokładnie na tę samą widok?

    Jeśli tak, URL zazwyczaj jest lepszym miejscem przechowywania tych danych.

    Pytanie 3: Czy przeglądarka może przechowywać to dane do momentu wysłania formularza?

    Jeśli wartość pola ma znaczenie tylko w momencie wysyłki, może wystarczyć niekontrolowany element wejścia.

    Zamiast:

    const [email, setEmail] = useState('');
    

    może wystarczyć po prostu:

    <input name="email" type="email" />
    

    i odczytać ją później za pomocą:

    const formData = new FormData(e.currentTarget);
    

    Pozwól przeglądarce zarządzać tym, co już potrafi obsłużyć.

    7. Co naprawdę należy do stanu w React

    Żadne z tego nie jest argumentem za całkowitym usunięciem stanu komponentów.

    Próba jego całkowitego usunięcia byłaby nadmierną korektą.

    Bardzo wiele wartości rzeczywiście nadaje się do przechowywania w stanie.

    Naprzykład:

    const [isOpen, setIsOpen] = useState(false);
    

    Tymczasowy menu rozwijane lub okno modalne wcale nie musi być odzwierciedlone w adreście URL.

    Interaktywny stan

    const [activeStep, setActiveStep] = useState(1);
    

    Gdy wartość steruje interakcją, która odbywa się wyłącznie w obrębie jednego komponentu, przechowywanie jej w lokalnym stanie jest często właściwym rozwiązaniem.

    Wartości, które zmieniają się niezależnie

    Jeśli coś rzeczywiście ma swój własny cykl życia i nie ma sposobu, by obliczyć go na podstawie już posiadanych danych, wtedy stan jest odpowiednim miejscem dla niego.

    Optymistyczne interfejsy użytkownika

    Czasami trzeba pokazać coś na ekranie, zanim serwer faktycznie potwierdzi, że to się wydarzyło.

    To również jest uzasadnionym zastosowaniem stanu.

    Cel nie polega na przestrzeganiu tej zasady:

    "Używaj mniej useState, bez względu na wszystko."

    Zamiast tego cel jest taki:

    "Upewnij się, że każdy element stanu ma odpowiedniego właściciela."

    Czasami odpowiednim właścicielem jest React.

    Czasami to URL.

    Czasami to DOM.

    Czasami to serwer.

    Czasami to dedykowana warstwa pobierania danych lub zarządzania stanem.

    To właśnie decyzja dotycząca własności naprawdę kształtuje architekturę aplikacji.

    8. Skup się na tym, kto posiada stan, a nie tylko na tym, ile go masz

    Dobrze zbudowana aplikacja React nie jest definiowana przez małą liczbę zmiennych stanu.

    Najważniejsze jest przydzielenie każdemu elementowi stanu jasnego, właściwego właściciela.

    Weźmy panel sterowania, który śledzi:

    Search query
    Filters
    Pagination
    Sort order
    Form values
    Server data
    Loading status
    Selected row
    Modal visibility
    Derived totals
    

    Kuszące jest umieszczenie wszystkich tych elementów bez zastanowienia do useState.

    Jednak te wartości nie pełnią tego samego zadania, więc nie powinny znajdować się w tym samym miejscu.

    Bardziej przydatny model myślowy dzieli je w następujący sposób:

    URL

    • Szukanie
    • Filtrzy
    • Paginacja
    • Kolejność sortowania
    • Karty do udostępniania

    DOM

    • Wartości formularza, które mają znaczenie tylko po jego wysłaniu

    Server / warstwa danych

    • Dane klientów
    • Odpowiedzi API
    • Dane serwera w pamięci cache’u

    Wartości pochodne

    • Liczby
    • Sumy
    • Listy filtrowane
    • Podsumowania walidacji

    Stan React

    • Widoczność modalu
    • Tymczasowe wybory
    • Stan animacji
    • Krótkotrwałe interakcje

    Gdy w ten sposób rozdzielisz stan, komponent będzie miał o wiele mniej elementów do synchronizacji.

    To właśnie dzięki tej redukcji pojawia się rzeczywista korzyść.

    9. Podwójny stan to źródło błędów jutro

    Najbardziej ryzykowny stan w aplikacji to niekoniecznie najskomplikowanszy.

    Często jest to stan, który istnieje jednocześnie w więcej niż jednym miejscu.

    Załóżmy, że masz:

    const [products, setProducts] = useState([]);
    const [filteredProducts, setFilteredProducts] = useState([]);
    

    Teraz to ty jesteś odpowiedzialny za upewnienie się, że filteredProducts zawsze odpowiada zawartości products.

    Dodając jeszcze jeden filtr, otrzymujesz:

    const [filteredProducts, setFilteredProducts] = useState([]);
    const [searchResults, setSearchResults] = useState([]);
    

    Teraz pojawia się dodatkowa zależność, którą musisz ręcznie utrzymywać w spójności.

    Ranо czy późno ktoś zaktualizuje podstawowe dane, ale zapomni zaktualizować jedną z zmiennych pochodnych, które od nich zależą.

    Interfejs po cichu traci synchronizację z rzeczywistością.

    Klient w końcu zgłasza coś takiego:

    ">Liczba wskazuje 42 produkty, ale na ekranie pokazuje się tylko 39."

    To niezgodność zazwyczaj nie jest błędem samej logiki filtrowania.

    Jest to objaw tego, że aplikacja ma więcej niż jedno źródło prawdy dla tych samych informacji.

    Jeśli zamiast tego wyznaczysz tę wartość:

    const filteredProducts = products.filter(...);
    

    Znika cała klasa błędów, zanim zdążą wystąpić.

    Dlatego redukcja stanu nie dotyczy wyłącznie wydajności renderowania.

    Chodzi tu przede wszystkim o zmniejszenie liczby elementów, które mogą stracić synchronizację.

    10. Koszt stanu wykracza poza ponowne renderowanie

    Gdy ludzie rozmawiają o stanie, rozmowa zwykle koncentruje się na wydajności: „Czy to spowoduje kolejne renderowanie?”

    To uzasadniona obawa, ale nie jest to najdroższy element całej sytuacji.

    Prawdziwym kosztem jest złożoność.

    Każda dodatkowa zmienna stanu to kolejna rzecz, którą zespół musi mieć na uwadze.

    Następnie nieuchronnie dodaje się wywołanie funkcji setter.

    setSomething(...)
    

    Następnie, prawdopodobnie, useEffect, aby zareagować na to.

    useEffect(...)
    

    Potem tablica zależności wymieniająca wartości, które są monitorowane.

    [firstValue, secondValue]
    

    Następnie jakaś logika służąca do przywrócenia wartości do stanu początkowego.

    setSomething(initialValue)
    

    Następnie przypadek krawędziowy, w którym ten stan musi pozostać zgodny ze zmianą nawigacji.

    Następnie zgłoszenie błędu, ponieważ stan nie został wyzerowany po pewnej modyfikacji.

    Krok po kroku baza kodu przekształca się w sieć logiki synchronizacji.

    To znacznie trudniejsze do zrozumienia niż w przypadku komponentu, którego dane pochodzą w sposób uporządkowany z kilku dobrze zdefiniowanych źródeł.

    Prawdziwa umiejętność w React nie polega na zarządzaniu większą ilością stanu

    Polega na zrozumieniu gdzie każdy element stanu powinien się znajdować.

    Mniej doświadczony programista patrzący na interfejs użytkownika może zapytać:

    ">Który stan jest tu potrzebny?"

    Lepszy instynkt architektoniczny prowadzi do innego pytania:

    „Które z tych wartości naprawdę muszą być przechowywane w stanie aplikacji?”

    A najbardziej precyzyjna wersja tego pytania brzmi:

    „Kto jest prawowitym właścicielem każdej części stanu aplikacji?”

    Zmiana sposobu myślenia w ten sposób może całkowicie odmienić podejście do tworzenia aplikacji React.

    Adres URL może być odpowiedzialny za stan nawigacji.

    Brauzer może zarządzać stanem formularzy.

    Serwer może przechowywać dane należące do niego.

    Wartości pochodne mogą pozostać takie, jakie są, zamiast być kopiowane.

    Unie w TypeScript mogą modelować stany, które są wzajemnie wykluczające się.

    A sam React może skupić się na krótkotrwałym, interaktywnym stanie, który faktycznie należy do warstwy interfejsu użytkownika.

    Taka podział pracy prowadzi do znacznie bardziej uporządkowanej architektury.

    Podsumowanie

    Mieć więcej stanów nie jest samym w sobie osiągnięciem.

    Zazwyczaj oznacza to jedynie większą ilość pracy związanej ze synchronizacją, więcej zależności do śledzenia oraz większe szanse na niezgodność danych.

    Zanim użyjesz kolejnego useState, przeanalizuj następujące pytania:

    1. Czy tę wartość można obliczyć podczas renderowania? Jeśli tak, wyprowadź ją zamiast przechowywać osobno.
    2. Czy musi przetrwać odświeżenie strony lub być udostępnialna za pomocą linku? Jeśli tak, umieść ją w parametrach wyszukiwania URL.
    3. Czy to pole formularza musi być śledzone przy każdym naciśnięciu klawisza? Jeśli nie, kontrolowany input w połączeniu z FormData może wystarczyć.
  • Czy kilka flag boolowskich faktycznie opisuje jeden podstawowy cykl życia? Zjednocz je w jedną wartość stanu lub maszynę stanów.
  • Czy ten stan jest tworzony tylko po to, by utrzymać go zsynchronizowanego z innym stanem? Jeśli tak, prawdopodobnie przechowujesz coś, co można było po prostu wywnioskować.
  • Czy ta wartość rzeczywiście ma swój własny cykl życia? Jeśli nie, najprawdopodobniej w ogóle nie musi być stanem.
  • Celem nigdy nie było minimalizowanie liczby surowych wywołań useState.

    Celem jest upewnienie się, że jako najmniej wartości odpowiada za określanie prawdy w twojej aplikacji.

    Ponieważ każdy niepotrzebny element stanu to kolejna rzecz, którą aplikacja musi synchronizować.

    A każdy krok synchronizacji, którego unikniesz, to błąd, którego nigdy nie będziesz musiał naprawiać.