Strona główna / Artykuły / Tworzenie modelu mentalnego dla Reacta: synchronizacja, stan i hooki

Tworzenie modelu mentalnego dla Reacta: synchronizacja, stan i hooki

Przyswoi się logikę leżącą u podstaw kluczowych koncepcji Reacta — synchronizację, komponenty, propsy, stan oraz hooksy — aby rozwijać intuicję zamiast zapamiętywać API.

2998 słów

Jeśli już potrafisz pisać kod, ale nie zagłębiałeś się jeszcze w React – albo tylko krótko go próbowałeś – ten tekst został napisany specjalnie dla ciebie.

Gdy zaczynasz uczyć się React, jest kuszące od razu przejść do useState, useEffect, props, hooków oraz długiej listy innych API. Możesz opanować składnię, stworzyć coś, co działa, ale nadal nie zrozumieć dlaczego React zachowuje się w taki sposób.

Cel tego artykułu to wypełnienie tej luki.

Zamiast traktować React jako zbiór API do zapamiętania, przyjrzymy się uzasadnieniom stojącym za jego projektem oraz temu, jak poszczególne elementy ze sobą współpracują. Składnia jest ważna, ale staje się o wiele łatwiejsza do zrozumienia, gdy poznasz to, co dzieje się pod spodem. Dlatego zamiast zaczynać od jak pisać kod w React, najpierw opracujemy jak myśleć o React.

Będziemy omawiać komponenty, propsy, stan, renderowanie, rekonsolidację, hooki, propagację propów, Context oraz routowanie, łącząc każdą koncepcję z problemem, który miała rozwiązać. Celem nie jest przedstawienie całego katalogu API dostępnych w React, lecz zapewnienie Ci modelu mentalnego, który pomoże Ci zrozumieć te API po ich pierwszym spotkaniu.

Prawdopodobnie słyszałeś, że jeden z powodów tak szybkiego działania Reacta to jakiś sprytny algorytm. Może to brzmieć niemal jak magia — jak biblioteka w JavaScriptu potrafi tak efektywnie aktualizować interfejs użytkownika?

Ten algorytm nazywa się rekonsolidacją, i to dobry punkt wyjścia do zrozumienia działania Reacta.

Rekonsolidacja — algorytm, który umożliwił istnienie Reacta

Porównywanie dwóch drzew DOM od zera oraz obliczanie minimalnego zestawu zmian jest zadaniem, którego rozwiązanie może zajmować około O(n³) czasu.

Ale załóżmy, że proces synchronizacji opiera się na kilku rozsądnych założeniach, a my dostarczamy mu kilka wskazówek dotyczących tego, gdzie najprawdopodobniej znajdują się zmiany?

I to wszystko! Złożoność spada do około O(n).

Oznacza to, że podejście, które w prosty sposób zajęłoby około 31 lat na zakończenie, skraca się do około 16 minut – wyłącznie dzięki założeniom i niewielkiej pomocy programisty.

Czekaj… wskazówki? Co dokładnie to są za wskazówki i jak je dostarczyć?

Nie ma potrzeby się stresować. To prostsze, niż się wydaje, i wkrótce to wyjaśnimy.

Na razie pozwólmy synchronizacji działać w tle i przenieśmy się do kwestii, która naprawdę jest ważna dla nas jako programistów.

Ale po co w ogóle używać React?

Dlaczego React?

Niezależnie od tego, co ci ktoś powie, przechodząc z zwykłych plików HTML, CSS i JavaScript do koncepcji takich jak komponenty, hooki, propsy i stan, napotykamy prawdziwy wyzwanie adaptacyjne.

W porównaniu z narzędziami takimi jak FastAPI czy Go, React rzeczywiście może sprawiać wrażenie wymagającego znacznie większych wysiłków.

Ale oto kluczowa kwestia: te trudności pojawiają się na początku.

Gdy zaczynasz lepiej rozumieć mechanizmy React, zdajesz sobie sprawę, że wiele tych na pierwszy rzut oka skomplikowanych koncepcji opiera się na zaskakująco prostych podstawach.

Nie musisz mieć całej aplikacji w pamięci naraz.

Zamiast tego możesz skupić się na jednym komponencie – jakich danych potrzebuje, co musi śledzić oraz jak powinien się aktualizować jego wyświetlany wynik.

To podzielone na części sposobu myślenia sprawia, że budowa i utrzymanie złożonych interfejsów staje się do zarządzania. I ostatecznie to jest wynik, który naprawdę interesuje programistów.

Pisanie kodu, który jest łatwiejszy do budowy, zrozumienia, modyfikacji i utrzymania. Celem jest tutaj pomoc w osiągnięciu tego celu.

Cztery filary Reacta :—

Komponenty

Komponent to w istocie funkcja JavaScript, która przyjmuje jeden obiekt i zwraca element interfejsu napisany w JSX.

JSX to rozszerzenie składni JavaScript, które umożliwia wstawianie znaczników przypominających HTML bezpośrednio do kodu JavaScript. Można stylizować elementy za pomocą zwykłego CSS lub biblioteki utility, takiej jak Tailwind CSS.

Każda wystarczająco złożona aplikacja React to ostatecznie jedynie sieć komponentów, które przekazują sobie dane.

Nawet jeśli nigdy wcześniej nie mieliście do czynienia z React, nadal możecie zrozumieć, co robi dany komponent.

const data = {
  question: "What is React?",
  answer: "A JavaScript library for building user interfaces"
};
function FlashCard(data) {
  return (
   <div className="border rounded-lg bg-gray-50 p-4">
       <h2>{data.question}</h2>
       <p>{data.answer}</p>
    </div>
   );
}

Jeśli zignorować kilka specyficznych dla React niuansów składniowych, to właśnie jest istota tego narzędzia.

Nieźle, prawda?

W gruncie rzeczy podajemy dane do funkcji i otrzymujemy element interfejsu użytkownika.

Jako programista twoim prawdziwym zadaniem jest projektowanie czystych komponentów, mając na uwadze trzy pytania:

  1. Jakie informacje on przyjmuje?
  2. Jakie informacje on przechowuje?
  3. Jak powinien się aktualizować wizualnie?

Cóż może być skomplikowanego w kilku parametrach i niektórych zmiennych? Czy nie moglibyśmy po prostu przekazać potrzebne dane i przechowywać to, co chcemy, w zmiennych lokalnych?

W pewnym sensie – ale nie do końca.

A co mamy na myśli, mówiąc o aktualizacji interfejsu? Wyobraźmy sobie dwóch sprzedawców, którzy próbują sprzedać ten sam samochód, ale menedżer poinformuje tylko jednego z nich o zmianie ceny.

Co się dzieje z drugim sprzedawcą? On nadal podaje stary cenę, nie mając pojęcia o zmianie.

A teraz wyobraźmy sobie, że menedżer publikuje tę aktualizację w grupowej rozmowie, do której należą wszyscy. Po jednej zmianie ceny cały zespół widzi ją natychmiast.

To właśnie jest problem, który miał rozwiązać React.

Każdy raz, gdy zmienia się jakaś informacja wpływająca na to, co jest pokazywane na ekranie, wszystko, co od niej zależy — osoby w naszej analogii, komponenty w React — potrzebuje niezawodnego sposobu na dowiedzenie się o tej zmianie i zareagowanie na nią. To właśnie taki mechanizm oferuje React.

Props

Czy function User(name, age, city) i function User(name, city) są wymienne? A co z function User(city, name)?

Typowa funkcja oparta na parametrach pozycyjnych może przyjąć tylko określoną liczbę argumentów w określonej kolejności — a pamiętaj, że komponent to po prostu funkcja.

Załóżmy teraz, że stworzyłeś komponent wyświetlający imię i wiek użytkownika, który jest obecnie używany w 67 różnych miejscach w twojej bazie kodu. Wtedy twój menedżer prosi cię, abyś pokazał również miasto użytkownika. Aktualizacja każdego z tych miejsc zajęłaby godziny pracy.

A co jeśli funkcja zostałaby zaprojektowana tak, aby stare wywołania nadal działały bez zmian, podczas gdy nowe wywołania mogłyby opcjonalnie przekazywać dodatkowe dane?

Narzędziem, którego tutaj potrzebujesz, jest pojedynczy obiekt reprezentujący listę parametrów. React nazywa to props.

Props to po prostu sposób, w jaki React łączy wszystkie dane przekazane komponentowi w jeden obiekt.

/** So a component like this one **/
function User({ name = "Guest", age = 18, city = "Unknown" }) {
    return (
        <div>
            <h2>{name}</h2>
            <p>{age} years old</p>
            <p>{city}</p>
        </div>
    );
}
/** Can be used in ways like **/
<User />                                    /** Guest, 18, Unknown **/
<User name="Pritam" />                      /** Pritam, 18, Unknown **/
<User name="Rahul" age={21} />              /** Rahul, 21, Unknown **/
<User name="Priya" age={22} city="Delhi" /> /** Priya, 22, Delhi **/

Stany

Czy menedżer salonu powinien zachować nową cenę dla siebie? Powiedzieć tylko jednemu sprzedawcy? Obu z nich? Czy też ogłosić ją wszystkim w salonie, włączając ochroniarzy i personel sprzątający?

Załóżmy skrzynię do przechowywania, którą wypełniamy naszymi rzeczami. Czy po prostu spojrzawszy na nią z zewnątrz, możemy stwierdzić, czy coś w środku się zmieniło? A jeśli skrzynia zostanie przeniesiona na inne półki? Przynajmniej zauważylibyśmy, że jej położenie się zmieniło, i wywnioskowalibyśmy, że coś się stało.

A teraz wyobraźmy sobie tę skrzynię jako mechanizm, którego używa React do przechowywania wartości, które zdefiniowaliśmy.

To oznacza, że musi istnieć sposób na przekazanie zaktualizowanych wartości tym, którzy od nich zależą, oraz sposób na to, by ci użytkownicy wiedzieli, kiedy ich wartość się zmieniła.

const [count, setCount] = useState(initialCount);

Czy spotkałeś się już z tą instrukcją useState?

Pasuje ona komponentowi dwie rzeczy:

  • count — aktualna wartość state.
  • setCount — funkcja, którą wywołujemy, aby poprosić o aktualizację tej wartości state.

Więc co jest nie tak z prostym napisaniem value = 5?

React nie ma sposobu, by dowiedzieć się, że coś wewnątrz tej struktury się zmieniło!

Innymi słowy, potrzebujesz mechanizmu, który poinformuje React: "Hej, zaktualizowałem tę wartość, być może powinieneś odświeżyć interfejs użytkownika."

Czy to jest wskazówka z wcześniej? W pewnym sensie tak, a w innym nie.

useState daje ci możliwość przechowywania wartości, żądania jej aktualizacji oraz jednoczesnego poinformowania React o dokonanej zmianie. Ale dlaczego trzeba to wyraźnie poinformować React?

Ponieważ w przeciwnym razie zachowywałbyś się jak ten nieostrożny menedżer salonu samochodowego.

Pamiętasz ten konkretny błąd? Menedżer zmienił cenę, zaktualizował swoją wersję tablicy i zapomniał powiedzieć o tym innym. Jesteś mądrzejszy – po prostu wywołasz setCount().

Co tak naprawdę się dzieje, gdy wywołasz setCount()?

React bierze nowy stan, ponownie renderuje komponent, aby ustalić, jak powinien teraz wyglądać interfejs użytkownika, a następnie korzysta z procesu porównywania, by określić, co faktycznie musi ulec zmianie na ekranie.

Po prostu poinformowałeś React, że coś się zmieniło. To proces porównywania określa co się zmieniło i co musi zostać zaktualizowane w wyniku tego.

Hooks

useState() — wbudowana funkcja, która pozwala komponentowi przechowywać fragment stanu i daje możliwość żądania jego aktualizacji.

Gdy ten stan zostanie zaktualizowany, mogą się wydarzyć dwie rzeczy:

  • Nowa wartość nie ma wpływu na to, co jest renderowane — React może nadal ponownie ją renderować, ale nic nie zmieni się widocznie.
  • Nowa wartość rzeczywiście wpływa na interfejs użytkownika — React ponownie renderuje komponent, porównuje nowy wynik z poprzednim poprzez proces synchronizacji i wprowadza tylko niezbędne zmiany w DOM.
  • Funkcje takie jak ta, które posiadają specjalne możliwości Reacta, nazywane są hookami. Warto poznać jeszcze kilka z nich.

    useRef() — przydatny, gdy potrzebujesz kontenera dla wartości, która w ogóle nie ma związku z interfejsem użytkownika.

    const count = useRef(0);
    count.current++;
    

    Zasada ogólna brzmi więc tak: jeśli zmiana wartości powinna również wpłynąć na interfejs użytkownika, użyj useState(). Jeśli tylko sama wartość musi ulec zmianie, bez żadnych konsekwencji w postaci renderowania, użyj useRef().

    Ma on również drugie powszechne zastosowanie — odnoszenie się do rzeczywistych elementów DOM — ale na razie ten model myślowy wystarczy.

    useEffect() — służy do wykonywania czegoś po zakończeniu renderowania interfejsu przez React.

    useEffect(() => {
        console.log("Runs after every render");
    });
    
    useEffect(() => {
        console.log("Runs once after the initial render");
    }, []);
    
    useEffect(() => {
        console.log("After the initial render and whenever count changes");
    }, [count]); /** Dependencies go here **/
    

    useContext() — wyobraźmy sobie drzewo komponentów, w którym dane takie jak name muszą przechodzić przez kilka warstw komponentów, aby dotrzeć do głęboko zagnieżdżonego komponentu UserName.

    A teraz rozszerzmy to drzewo o wiele więcej danych, które przechodzą przez komponenty, które nawet ich bezpośrednio nie używają.

    Taki wzorzec nazywa się prop drilling.

    API Context w React oferuje rozwiązanie tego problemu: umożliwia udostępnianie danych w dowolnym głębszym punkcie drzewa bez konieczności ręcznego przekazywania ich przez każdy komponent pośredni.

    const ParentContext = createContext(null)
    
    <ParentContext value={money}>
      <ChildComponent />
    </ ParentContext>
    
    function ChildComponent() {
      const money = useContext(ParentContext);
    
      return <p>Money: {money}</p>;
    }
    

    Istnieje o wiele więcej hooków poza tymi, a warto samodzielnie z nimi eksperymentować.

    React Router

    React umożliwia tworzenie interfejsów składających się z komponentów, które aktualizują się same, bez konieczności ponownego ładowania całej strony przez przeglądarkę. Jednak prawdziwa aplikacja zazwyczaj wymaga więcej niż jednej strony, co rodzi nowe pytanie.

    Co się dzieje, gdy aplikacja potrzebuje kilku różnych widoków?

    Możesz chcieć czegoś takiego:

    /home → Strona główna /dashboard → Panel sterowania /profile → Profil

    Jeśli połączysz je zwykłymi tagami HTML, kliknięcie w nie spowoduje pełną nawigację w przeglądarce. Cała strona zostanie usunięta, a aplikacja uruchomi się ponownie od zera pod nowym adresem.

    To, czego naprawdę chcesz, to aby URL się aktualizował, podczas gdy React cicho określi które komponenty należy zastąpić, bez usuwania wszystkiego innego.

    To właśnie jest problem, który rozwiązuje React Router.

    Pomyśl o nim jako o warstwie, która mapuje URL-y na komponenty.

    Na przykład:

    <BrowserRouter>
      <Routes>
        <Route path="/home" element={<Home />} />
        <Route path="/dashboard" element={<Dashboard />} />
      </Routes>
    </BrowserRouter>
    

    React Router sprawdza aktualny URL i renderuje ten komponent, który jest z nim powiązany. BrowserRouter polega na API Historii przeglądarki do obsługi nawigacji po stronie klienta, a Routes wybiera ten Route, który najlepiej pasuje do aktualnej ścieżki.

    Mimo to istnieje jeszcze jeden problem do rozwiązania.

    A co jeśli nie chcesz, aby cały ekran był ponownie renderowany?

    Zobaczmy taki układ:

    ┌─────────────────────────────┐
    │          Header             │
    ├──────────┬──────────────────┤
    │          │                  │
    │  Menu    │   Page content   │
    │          │                  │
    ├──────────┴──────────────────┤
    │          Footer             │
    └─────────────────────────────┘
    

    Przejście z /home na /dashboard nie powinno powodować znikania i ponownego pojawiania się nagłówka, bocznej kolumny ani stopki. Tylko główna obszara treści musi ulec zmianie.

    To dokładnie ten scenariusz, do którego zostały stworzone zagłębione trasy oraz <Outlet />.

    function Layout() {
      return (
        <>
          <Header />
          <Menu />
          <Outlet />
          <Footer />
        </>
      );
    }
    

    Same trasy mogą być następnie umieszczone jedna w drugiej:

    <Routes>
      <Route element={<Layout />}>
        <Route index element={<Home />} />
        <Route path="dashboard" element={<Dashboard />} />
      </Route>
    </Routes>
    

    Dzięki takiemu ustawieniu Layout pozostaje zamontowany, a React Router wstawia tę trasę potomną, która pasuje do zawartości wewnątrz <Outlet />. Zgodnie z dokumentacją React Router, <Outlet /> oznacza miejsce, w którym renderowana jest pasująca trasa potomna.

    Ścieżka index dopasowuje <Home /> do ścieżki /, co sprawia, że jest to domyślna widok pokazywany w outletu, gdy nie aktywna jest żadna bardziej specyficzna ścieżka.

    Zatem przechodzenie z /home na /dashboard nie polega tak naprawdę na zastąpieniu całej strony. Jest to bardziej podobne do stwierdzenia:

    ">Zostaw tę część interfejsu bez zmian i zastąp tylko tę sekcję odpowiednim komponentem należącym do nowej ścieżki."

    Co dalej?

    Gdy już dobrze zrozumiesz problemy, które ma rozwiązać React, oraz mechanizmy, których używa do ich rozwiązania, warto poświęcić czas na przeczytanie rzeczywistych baz kodu, aby zobaczyć, jak doświadczone zespoły strukturują swoje aplikacje.

    Znajdź repozytorium, które pokazuje, jak aplikacja React produkcyjna może być zorganizowana i zaprojektowana na dużą skalę.

    Zamiast próbować przyswoić całą bazę kodu za jednym razem, wybierz jeden konkretny funkcjonalność i śledź jego rozwój na poszczególnych poziomach aplikacji. Zwróć uwagę na to, w jaki sposób składniki są grupowane, skąd pochodzą dane, jak zarządzany jest stan oraz w jaki sposób poszczególne części aplikacji ze sobą komunikują się.

    Warto również znaleźć przykład pokazujący, jak React może być łączony z Redux w działającej aplikacji.

    Jeśli znaleziony przez ciebie projekt jest starszy, nie traktuj jego wzorców jako obecnego standardu dla React. Wykorzystaj go raczej do zrozumienia, jak dużą aplikację można podzielić na mniejsze części oraz jak Redux wpisuje się w tę strukturę.

    Gdy już opanujesz typową strukturę pliku w React i będziesz w stanie samodzielnie tworzyć proste składniki, następnym krokiem będzie nauka budowania aplikacji, które są wydajne w dużych skali.

    Skalowanie aplikacji React niesie ze sobą własne problemy, w tym:

    • SSR i komponenty serwerowe — jak zmienia się zachowanie aplikacji, gdy jej części działają na serwerze zamiast wyłącznie w przeglądarce.
    • Zarządzanie stanem — co robić, gdy stan aplikacji staje się zbyt duży lub jest zbyt szeroko udostępniany, by lokalny stan i Context mogły go skutecznie obsłużyć. Redux to jedna z kilku opcji.
    • Pobieranie danych i cacheowanie — jak aplikacje produkcyjne zarządzają wskaźnikami ładowania, obsługą błędów, cacheowaniem oraz utrzymywaniem synchronizacji danych klienckich z serwerem.
    • Wydajność — rozpoznawanie momentu, gdy renderowanie naprawdę staje się wąskim gardłem, oraz optymalizacja odpowiednio, zamiast optymalizować wszystko od razu.
  • Testowanie — sprawdzanie, czy komponenty oraz procesy użyteczności nadal działają poprawnie w miarę rozszerzania się bazy kodu.
  • Nie musisz opanować wszystkich tych tematów przed rozpoczęciem tworzenia.

    Bardziej praktycznym podejściem jest rozpoczęcie pracy nad projektem, napotkanie konkretnego problemu, a następnie poznanie takiej koncepcji lub narzędzia, które rozwiązuje ten problem.

    Ostatecznie celem nauki React nie było zapamiętywanie jego interfejsu API. Chodziło o zrozumienie dlaczego te interfejsy istnieją i jak myśleć o problemach, które mają rozwiązywać.

    Literatura pokrewna

  • Zrozumienie Proxy-a w JavaScriptie: Pułapki, Reflect i wzorce reaktywne — Dowiedz się, jak obiekty Proxy i Reflect w JavaScriptie przerywają dostęp do właściwości, co umożliwia wdrażanie mechanizmów walidacji, tworzenie właściwości wirtualnych oraz korzystanie z frameworków reaktywnych.
  • Zarządzanie stanami interfejsu w praktyce za pomocą warunkowego renderowania w React — Naucz się budować interfejsy do autoryzacji, określania ról i uprawnień, pokazywania stanów ładowania, błędów oraz stanu pustego w React przy użyciu praktycznych wzorców warunkowego renderowania.
  • React Components 101: Tworzenie komponentów UI wielokrotnie używalnych i łatwych w utrzymaniu — Dowiedz się, dlaczego dzielenie interfejsów użytkownika na małe komponenty React poprawia ich wielokrotną użyteczność, czytelność oraz współpracę zespołu, a następnie stwórz swój pierwszy funkcjonalny komponent.
  • Narzędziówka z wielokrotnie używalnymi niestandardowymi hookami dla każdego nowego projektu React — Poznaj starannie wyselekcjonowaną kolekcję niestandardowych hooków React – obejmujących funkcje do przechowywania danych, opóźniania wywołań, obsługi kliknięć oraz pobierania danych – które eliminują powtarzalne fragmenty kodu w nowych projektach.
  • Przestań synchronizować stan z useEffect: bardziej bezpieczny wzorzec Reacta — Dowiedz się, dlaczego używanie useEffect do synchronizacji stanu pochodnego powoduje sytuacje konkurencyjne i dodatkowe renderowania, oraz jak zastąpić to wyprowadzaniem wartości w czasie renderowania i użyciem atrybutu key.
  • Przemyśl stan Reacta: 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.