Strona główna / Artykuły / Od skryptów DOM do interfejsu użytkownika jako funkcji stanu: podstawowy model Reacta

Od skryptów DOM do interfejsu użytkownika jako funkcji stanu: podstawowy model Reacta

Dlaczego React zastępuje ręczne aktualizacje DOM-u deklaratywnymi komponentami oraz jak JSX, props, state, ponowne renderowanie i jednokierunkowy przepływ danych pasują do jednego modelu myślowego.

4868 słów

Klawisz w stylu „lajk”, który aktualizuje licznik, zmienia ikonę i odtwarza animację, można łatwo stworzyć za pomocą prostych wywołań DOM. Pięćdziesiąt takich elementów w transmisji na żywo, z których każdy śledzi również komentarze, udostępnienia i stan zapisany, to moment, w którym ręcznie pisany kod DOM zaczyna sprawiać problemy. React został stworzony po to, by rozwiązać tego typu problemy: opisujesz, jak powinien wyglądać ekran dla bieżących danych, a React określa, jakie zmiany w DOM są konieczne.

To przewodnik omawia idee, które to umożliwiają, w kolejności, w jakiej się one wzajemnie opierają: JSX, komponenty, propsy, stan, ponowne renderowanie, styl deklaratywny oraz sposób, w jaki dane i zdarzenia przepływają przez drzewo komponentów. Pod koniec powinieneś być w stanie przewidzieć, kiedy komponent zostanie ponownie zrenderowany, określić, gdzie powinien znajdować się dany element stanu, oraz zauważyć błędy początkujących, które zakłócają aktualizacje.

Problem, który miał rozwiązać React

Zanim biblioteki komponentów stały się normą, interaktywny interfejs oznaczał kod imperatywny: trzeba było znaleźć elementy, a następnie osobiście wprowadzać każdą zmianę w momencie, gdy coś się działo. W przypadku pojedynczego przycisku „Lajkuj” konfiguracja wygląda tak:

// Traditional DOM manipulation
const button = document.getElementById("like-button");
const countEl = document.getElementById("like-count");
const icon = document.getElementById("like-icon");
let liked = false;
let count = 42;

A obsługa kliknięcia musi pamiętać o każdym szczególe wizualnym obu stanów:

button.addEventListener("click", function() {
  if (!liked) {
    liked = true;
    count++;
    countEl.textContent = count;
    button.classList.add("liked");
    button.classList.remove("unliked");
    icon.src = "/icons/heart-filled.svg";
    button.style.color = "#e0245e";
    // trigger animation
    button.classList.add("animate-pop");
    setTimeout(() => button.classList.remove("animate-pop"), 300);
  } else {
    liked = false;
    count--;
    countEl.textContent = count;
    button.classList.remove("liked");
    button.classList.add("unliked");
    icon.src = "/icons/heart-empty.svg";
    button.style.color = "#6c757d";
  }
});

To jest do zarządzania przy jednym przycisku. Teraz wyobraźmy sobie feed z 50 postami, z których każdy ma własne przyciski do lajkowania, komentowania, udostępniania i zapisywania – wszystkie one mogą ulec zmianie pod wpływem kliknięć użytkownika lub nowych danych przychodzących z serwera. Skrypt zamienia się w sieć poszukiwań elementów, słuchaczy i zmiennych, które trzeba ręcznie synchronizować. W miarę rozwoju aplikacji pojawiają się trzy możliwe błędy:

  • Synchronizacja. DOM pokazuje jedno, a zmienne wskazują coś innego. Zaktualizujesz element licznika, ale zapomnisz o zmiennej, albo odwrotnie – wtedy masz dwa źródła prawdy do naprawiania.
  • Możliwość ponownego użycia. Obsługa zdarzeń jest powiązana z konkretnymi identyfikatorami elementów i określoną strukturą HTML, więc przeniesienie przycisku na inną stronę oznacza konieczność jego ponownego napisania.
  • Złożoność. Każda nowa funkcjonalność zmusza do zrozumienia wszystkich innych elementów, na które może wpłynąć. Małe zmiany mogą uszkodzić odległe części strony.

React, udostępniony na licencji otwartej przez Facebooka w 2013 roku, odpowiada na wszystkie te problemy jedną ideą: przestań wydawać polecenia do DOM i zamiast tego opisz interfejs, który powinien istnieć w danym stanie. React porównuje tę opis z tym, co jest na ekranie, i aplikuje różnice. Ty opisujesz; React aktualizuje.

JSX: markup, który kompiluje się do JavaScript

Pierwszą nietypową rzeczą w kodzie React jest markup przypominający HTML znajdujący się wewnątrz funkcji:

function Greeting() {
  return (
    <div className="greeting">
      <h1>Hello, Priya!</h1>
      <p>Welcome back.</p>
    </div>
  );
}

Ten markup to JSX – rozszerzenie składni, które umożliwia pisanie drzew elementów w plikach JavaScript. Nie jest to ani HTML, ani cokolwiek, co rozumie przeglądarka. Krok kompilacji (Babel, kompilator TypeScript lub narzędzie do pakowania, które je wykorzystuje) przepisuje go na zwykłe wywołania funkcji przed uruchomieniem kodu. Pisze się:

// What you write (JSX):
return (
  <h1 className="title">Hello!</h1>
);

a kompilator tworzy coś równoważnego:

// What the compiler transforms it into:
return React.createElement("h1", { className: "title" }, "Hello!");

React.createElement zwraca po prostu zwykły obiekt opisujący element: jego typ, atrybuty i dzieci. React analizuje te obiekty, aby ustalić, co powinno znajdować się w rzeczywistym DOM-ie. (Nowsze transformacje JSX używają zamiast tego innego narzędzia z biblioteki react/jsx-runtime, ale wynikiem jest ten sam rodzaj obiektu opisowego.) Nikt nie pisze takich wywołań ręcznie; JSX istnieje dlatego, że zagnieżdżone tagi są znacznie łatwiejsze do odczytania.

Gdzie JSX różni się od HTML

Syntakta jest podobna do HTML, ale nie identyczna. Najczęściej mylące różnice to:

// HTML                              JSX
// ─────────────────────────────     ──────────────────────────────────
// class="title"                    className="title"  ← JS reserved word
// for="email"                      htmlFor="email"    ← JS reserved word
// <input> (self-closing optional)  <input />          ← must self-close
// onclick="handler()"              onClick={handler}  ← camelCase, no quotes
// style="color: red"               style={{ color: "red" }}  ← JS object

class i for to słowa rezerwowane w JavaScript, dlatego JSX używa className i htmlFor. Każdy element musi być zamknięty. Obsługi zdarzeń mają nazwę w stylu camelCase i przyjmują funkcję, a nie łańcuch tekstowy. Stylizacje wewnątrz elementu to obiekty. Aby lepiej zrozumieć zalety i wady tej składni, zapoznaj się z artykułem dlaczego JSX nie jest HTML.

Zawieranie wyrażeń JavaScript

Figurki nawiasowe otwierają możliwości JavaScript. Wewnątrz mogą znajdować się wszystko, co daje wartość: zmienne, wywołania funkcji, operatory ternarne, metody tablic. W tym przykładzie najpierw obliczany jest status online:

function UserCard({ user }) {
  const isOnline = user.lastSeen < Date.now() - 5 * 60 * 1000;

a następnie kilka wyrażeń kształtuje treść: skrócona biografia, warunkowa nazwa klasy, warunkowe etykieta oraz sformatowana data:

  return (
    <div className="card">
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      <p>{user.bio.length > 100 ? user.bio.slice(0, 100) + "..." : user.bio}</p>
      <span className={isOnline ? "badge-green" : "badge-grey"}>
        {isOnline ? "Online" : "Offline"}
      </span>
      <p>Joined: {new Date(user.joinedAt).toLocaleDateString()}</p>
    </div>
  );
}

Zasada jest prosta: nawiasy zawierają wyrażenia, a nie instrukcje. Można użyć operacji ternarnej, ale nie bloku if, oraz metody .map(), ale nie pętli for, bezpośrednio wewnątrz markupu.

Komponenty: funkcje zwracające interfejs użytkownika

Komponent to wielokrotnie używalny, samodzielny element interfejsu napisany jako funkcja JavaScript, która zwraca JSX. To cała definicja:

// A component is just a function that returns JSX
function Button() {
  return (
    <button className="btn">
      Click me
    </button>
  );
}

Używa się go w taki sam sposób jak tagu HTML, i można umieścić ich tyle, ile się chce:

function App() {
  return (
    <div>
      <Button />
      <Button />
      <Button />
    </div>
  );
}

To renderuje trzy identyczne przyciski na podstawie jednej definicji.

Komponowanie małych komponentów w większe

Komponenty stają się potężne, gdy są nawzajem zawierane. Małe, jednoznaczne elementy łączy się w większe, niczym klocki budulcowe. Awatar wie tylko, jak wyświetlać obrazek:

function Avatar({ src, alt }) {
  return <img className="avatar" src={src} alt={alt} />;
}

A na tym buduje się łańcuch nieco większych komponentów: blok z nazwą, wiersz informacji o użytkowniku łączący awatar z nazwą oraz karta posta łącząca informacje o użytkowniku z treścią posta:

function UserName({ name, handle }) {
  return (
    <div>
      <strong>{name}</strong>
      <span>@{handle}</span>
    </div>
  );
}function UserInfo({ user }) {
  return (
    <div className="user-info">
      <Avatar src={user.avatar} alt={user.name} />
      <UserName name={user.name} handle={user.handle} />
    </div>
  );
}function PostCard({ post, author }) {
  return (
    <article className="post-card">
      <UserInfo user={author} />
      <p>{post.content}</p>
      <span>{post.likes} likes</span>
    </article>
  );
}

Każda warstwa ma jedno zadanie. Avatar renderuje obrazek, UserInfo umieszcza awatar obok nazwy, a PostCard strukturyzuje cały post. To właśnie taki sposób pracy promuje React: dzielenie interfejsu na najmniejsze, sensowne części, przypisanie każdej z nich jasnej responsybilności oraz ich łączenie. przewodnik po tworzeniu wielokrotnie używalnych komponentów React szczegółowo omawia projektowanie komponentów.

Dlaczego nazwy komponentów są wielkie

React odróżnia elementy i komponenty natywne na podstawie pierwszej litery. <button> staje się zwykłym przyciskiem DOM, natomiast <Button> sprawia, że React szuka w zakresie zmiennej o nazwie Button i ją wywołuje. Komponent nazwany małą literą jest potajemnie traktowany jako nieznana tag HTML, co często powoduje zamieszanie typu „mój komponent nic nie wyświetla”.

Props: konfiguracja komponentu z zewnątrz

Przycisk, który zawsze pokazuje napis „Kliknij mnie”, nie jest zbyt przydatny. Props (skrót od properties) to sposób, w jaki komponent nadrzędny przekazuje dane do komponentu potomnego, dzięki czemu jedna definicja może być użyta w wielu sytuacjach.

Props przemieszczają się w jednym kierunku, od komponentu, który renderuje inny, do tego, który jest renderowany, i przybywają jako pojedynczy argument typu obiekt. Komponent nadrzędny zapisuje je jako atrybuty:

// Parent passes data as props (looks like HTML attributes)
function App() {
  return (
    <div>
      <Button label="Submit" colour="blue" />
      <Button label="Cancel" colour="grey" />
      <Button label="Delete" colour="red" />
    </div>
  );
}

a potomkowie dekonstruują to, czego potrzebują:

// Child receives them as an object
function Button({ label, colour }) {
  return (
    <button className={`btn btn-${colour}`}>
      {label}
    </button>
  );
}

Jedna definicja, trzy przyciski o różnym wyglądzie, ponieważ każdy otrzymał inne wartości.

Właściwości mogą przenosić dowolne wartości

Strony tekstowe to dopiero początek. Liczby, wartości logiczne, tablice, obiekty i funkcje to wszystko ważne właściwości. Na przykład karta produktu może zawierać obiekt danych, funkcję zwrotną i flagę:

function ProductCard({ product, onAddToCart, featured }) {
  return (
    <div className={`card ${featured ? "card-featured" : ""}`}>
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>₹{product.price.toLocaleString()}</p>
      <p>{product.rating} ★ ({product.reviewCount} reviews)</p>
      <button onClick={onAddToCart}>
        Add to Cart
      </button>
    </div>
  );
}

W miejscu wywołania te wartości są przekazywane za pomocą nawiasów:

// Usage:
<ProductCard
  product={{ name: "Headphones", price: 2499, rating: 4.3, reviewCount: 128 }}
  onAddToCart={() => addToCart(product.id)}
  featured={true}
/>

Uwaga: callback inline’owy onAddToCart odnosi się do product.id, ale product w tym przypadku to jedynie literal obiektu przekazany jako właściwość, a nie zmienna w zakresie dostępu. W prawdziwym kodzie należałoby użyć zmiennej zdefiniowanej w komponencie nadrzędnym, na przykład onAddToCart={() => addToCart(item.id)}. Przekazywanie funkcji jako właściwości to standardowy sposób, w jaki komponenty potomne mogą zgłaszać zdarzenia, co zostanie ponownie omówione poniżej.

Właściwości są tylko do odczytu

Komponent nie powinien nigdy zmieniać swoich własnych właściwości. Podczas renderowania są to stałe dane wejściowe. Gdy coś musi ulec zmianie, komponent nadrzędny to zmienia i renderuje komponent potomny z nową wartością. Przekształcanie właściwości wewnątrz komponentu potomnego to błąd:

// WRONG — never modify props
function Button({ count }) {
  count = count + 1; // ← this is a mistake
  return <button>{count}</button>;
}

Ponowne przypisanie zmienia jedynie zmienną lokalną; nigdy nie dociera do zmiennych nadrzędnych i znika podczas następnego renderowania. Gdy komponent potrzebuje wartości, którą może zmieniać, właśnie do tego służy stan:

// CORRECT — props are read-only, use state for data that changes
function Button({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Tutaj initialCount jedynie inicjuje stan; po pierwszym renderowaniu komponent sam zarządza swoją liczbą.

Stan: własna pamięć komponentu

Propy pochodzą z zewnątrz. Stan to dane, którymi dysponuje komponent i które może zmieniać z upływem czasu. Gdy stan się zmienia, React ponownie renderuje komponent z nową wartością, a ty sam nigdy nie manipulujesz DOM-em.

Stan jest tworzony za pomocą hooka useState, importowanego z React:

import { useState } from "react";

Licznik pokazuje podstawową strukturę:

function Counter() {
  const [count, setCount] = useState(0);
  //     ↑ current value   ↑ function to update it    ↑ initial value  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

useState(0) zwraca parę: aktualną wartość (count, która zaczyna się od 0) oraz funkcję do ustawiania tej wartości (setCount). Wywołanie tej funkcji z nową wartością informuje React o konieczności jej przechowania i ponownego renderowania komponentu.

Odbudowa przycisku „Lubię” przy użyciu stanu

Imperatywny przycisk „Lubię” z początku staje się znacznie prostszy. Zacznijmy od importu:

import { useState } from "react";

a następnie opisz przycisk dla każdej kombinacji wartości liked i count:

function LikeButton({ initialLikes }) {
  const [liked, setLiked] = useState(false);
  const [count, setCount] = useState(initialLikes);  function handleClick() {
    if (liked) {
      setLiked(false);
      setCount(c => c - 1);
    } else {
      setLiked(true);
      setCount(c => c + 1);
    }
  }  return (
    <button
      onClick={handleClick}
      className={liked ? "btn-liked" : "btn-default"}
    >
      {liked ? "♥" : "♡"} {count}
    </button>
  );
}

Nie ma żadnych zapytań o elementy, żadnych wywołań classList ani przypisów do textContent. Funkcja obsługi aktualizuje dwie wartości stanu, a JSX określa wygląd przycisku dla tych wartości. React dopasowuje wtedy DOM do tych danych.

Zwróć uwagę na formularz aktualizacji setCount(c => c - 1). Przekazanie funkcji zamiast wartości pozwala uzyskać najnowszy stan, co jest bezpieczniejsze, gdy w tym samym wydarzeniu znajduje się kilka aktualizacji.

Każda instancja przechowuje swój własny stan

Stan należy do konkretnej instancji komponentu, a nie do definicji komponentu. Wyświetl trzy takie przyciski – każdy z nich ma swój własny liked i count; kliknięcie jednego nie wpływa na pozostałe:

function Feed() {
  return (
    <div>
      <LikeButton initialLikes={24} />   {/* has its own state */}
      <LikeButton initialLikes={7} />    {/* has its own state */}
      <LikeButton initialLikes={156} />  {/* has its own state */}
    </div>
  );
}

Co powinno znajdować się w stanie

Używaj stanu dla wartości, które:

  • zmieniają się z upływem czasu pod wpływem interakcji lub nowych danych
  • powinny aktualizować interfejs użytkownika w momencie zmiany
  • należą do tej konkretnej instancji komponentu

Unikaj przechowywania w stanie wszystkiego, co można obliczyć na podstawie istniejących właściwości lub stanu (lepiej oblicz to podczas renderowania), oraz wartości, które zmieniają się bez konieczności ponownego renderowania, takich jak identyfikator timera, który lepiej pasuje do ref.

Jak działa ponowne renderowanie

Ponowne renderowanie jest silnikiem całego modelu. Gdy stan się zmienia, React ponownie wywołuje funkcję komponentu, uzyskuje nowy opis interfejsu użytkownika, porównuje go z poprzednim i aktualizuje tylko te części DOM, które się różnią.

Podczas pierwszego renderowania licznik tworzy swoją strukturę markup, a React buduje węzły DOM:

Initial render:
  count = 0
  Component runs → returns <p>Count: 0</p> <button>Increment</button>
  React creates DOM nodes

Gdy użytkownik kliknie, funkcja setter uruchamia nowe renderowanie, a React porównuje oba opisy:

User clicks Increment:
  setCount(1) called
  React re-renders the component
  count = 1
  Component runs again → returns <p>Count: 1</p> <button>Increment</button>
  React compares: <p>Count: 0</p> vs <p>Count: 1</p>
  React updates only the text node inside <p>
  Button is unchanged — React leaves it alone

Tylko tekst wewnątrz akapitu zmienia się w rzeczywistym DOM-ie. Przycisk pozostaje nietknięty. React nie odbudowuje strony; oblicza najmniejszy zbiór zmian i stosuje tylko je, dlatego częste ponowne renderowanie zazwyczaj nie stanowi problemu.

Co powoduje ponowne renderowanie komponentu

Komponent jest renderowany ponownie, gdy zmienia się jego własny stan:

1. The component's own state changes (setCount, setUser, etc.)
         ↓
   Component re-renders

Renderuje się również ponownie w kilku innych sytuacjach:

2. The component's props change (parent passes different values)
         ↓
   Component re-renders3. A context the component uses changes
         ↓
   Component re-renders4. The parent re-renders
         ↓
   All children re-render (unless memoized)

W praktyce „zmienione props” i „ponowne renderowanie rodzica” to ten sam zdarzenie widziane z dwóch perspektyw: dziecko otrzymuje nowe props tylko dlatego, że jego rodzic został ponownie renderowany. Praktyczną konsekwencją jest to, że domyślnie renderowanie rodzica przenosi się na wszystkie jego dzieci, niezależnie od tego, czy ich props się zmieniły, czy nie, chyba że są zmemorizowane.

Ta kaskada rzadko stanowi problem. Render to po prostu wywołanie funkcji, które tworzy obiekty; kosztowną częścią jest modyfikacja rzeczywistego DOM-u, czego React stara się unikać w maksymalnym stopniu. Większość prawdziwych problemów z wydajnością wynika ze sposobu strukturyzacji komponentów i stanu, a nie z samej liczby renderowań.

Wirtualny DOM i proces porównywania

React przechowuje lekką reprezentację interfejsu użytkownika w formie JavaScriptu, często nazywaną wirtualnym DOM-em. Każdy render tworzy nowy drzewo obiektów elementów, a React porównuje je z poprzednim drzewem w procesie zwanym porównywaniem. Rezultatem tego porównania jest lista operacji na rzeczywistym DOM-ie, które należy wykonać.

Nigdy nie pracujesz bezpośrednio z tą warstwą; jest to szczegół implementacji. Ma to znaczenie, ponieważ porównywanie zwykłych obiektów w pamięci jest tanie, podczas gdy rzeczywiste operacje DOM są stosunkowo wolne. Przekierowywanie każdej aktualizacji przez to porównanie pozwala React grupować i minimalizować kosztowne operacje.

Interfejs użytkownika deklaratywny versus imperatywny

React jest deklaratywny, a zrozumienie tego pojęcia to prawdziwa zmiana sposobu myślenia. Porównaj dwa sposoby renderowania listy.

Imperatywny: wyliczanie kroków

Wersja imperatywna najpierw opróżnia kontener:

// Imperative: manual DOM manipulation
const list = document.getElementById("list");
list.innerHTML = "";  // clear it

następnie ręcznie tworzy, konfiguruje i dodaje każdy element:

items.forEach(item => {
  const li = document.createElement("li");
  li.textContent = item.name;
  li.className = item.active ? "active" : "";
  li.addEventListener("click", () => handleClick(item.id));
  list.appendChild(li);
});

Kod to sekwencja poleceń: utwórz ten węzeł, ustaw tę właściwość, dołącz tego słuchacza, wstaw go tam. Jesteś odpowiedzialny za to, co dzieje się przed, po oraz na każdym kroku pomiędzy.

Deklaratywny: opisanie rezultatu

Wersja deklaratywna określa, jak powinien wyglądać list dla dowolnego tablicy items:

// Declarative: describe the desired output
function ItemList({ items, onItemClick }) {
  return (
    <ul>
      {items.map(item => (
        <li
          key={item.id}
          className={item.active ? "active" : ""}
          onClick={() => onItemClick(item.id)}
        >
          {item.name}
        </li>
      ))}
    </ul>
  );
}

Nie ma takiego rozwiązania jak „wyłączenie listy, a następnie jej odbudowa”. Opisujesz docelowy stan, a React sam decyduje, jak dostać się z obecnego DOM-u do tego celu. Atrybut key informuje React, który element jest którym pomiędzy kolejnymi renderowaniami, dzięki czemu może przenieść, zaktualizować lub usunąć odpowiednie elementy zamiast tworzyć je od nowa.

Dlaczego styl deklaratywny pasuje do interfejsów użytkownika

  • Predykowalność. Przy tych samych atrybutach i stanie komponent renderuje ten sam wynik. Możesz traktować interfejs jako czystą funkcję swoich danych: UI = f(state, props).
  • Mniej do zapamiętywania. Nie musisz śledzić, jak obecnie wygląda DOM ani jakie zmiany są potrzebne. Opisujesz tylko obecny stan.
  • Mniej błędów. Większość błędów związanych z ręcznym zarządzaniem DOM-em, takich jak aktualizacja jednego elementu bez jego sąsiada lub niezgodność słuchaczy z danymi, po prostu nie może wystąpić, gdy cała widok jest wyprowadzana ze stanu.
  • Drzewo komponentów i jednokierunkowy przepływ danych

    Każda aplikacja React to drzewo. Komponent korzeniowy, zazwyczaj App, renderuje swoje dzieci, które z kolei renderują swoje własne dzieci, odzwierciedlając strukturę ekranu:

    App
    ├── Navbar
    │   ├── Logo
    │   ├── NavLinks
    │   └── UserMenu
    │       ├── Avatar
    │       └── DropdownMenu
    ├── Dashboard
    │   ├── Sidebar
    │   │   ├── SidebarLink (×5)
    │   │   └── UserStats
    │   └── MainContent
    │       ├── StatsRow
    │       │   ├── StatCard (×4)
    │       └── RecentActivity
    │           ├── ActivityItem (×10)
    └── Footer
    

    Dane płyną w dół

    Dane przechodzą od rodzica do dziecka za pośrednictwem propów. Komponent nie może uzyskać dostępu do stanu swojego brata w drzewie lub rodzica. To jednokierunkowy przepływ sprawia, że duże aplikacje pozostają zrozumiałe:

    App (has user, notifications, theme)
      │
      ├── Navbar (receives: user, notifications)
      │     │
      │     └── UserMenu (receives: user)
      │           │
      │           └── Avatar (receives: user.avatar, user.name)
      │
      └── Dashboard (receives: user, theme)
            │
            └── MainContent (receives: theme)
    

    Avatar widzi tylko to, co dostaje od UserMenu, a UserMenu widzi tylko to, co dostaje od Navbar. Nic nie ucieka na boki ani w górę, więc gdy wartość jest błędna, należy sprawdzić łańcuch rodziców.

    Przepływ zdarzeń w górę

    Komponent znajdujący się głęboko w drzewie nie może bezpośrednio zmieniać stanu swojego rodzica, ale może wywołać funkcję przekazaną przez niego. Stan należy do rodzica:

    // Parent owns the state and passes down both the value and the updater
    function App() {
      const [searchQuery, setSearchQuery] = useState("");
    

    i przekazuje zarówno wartość, jak i funkcję do ustawienia swoim dzieciom; pasek wyszukiwania wywołuje tę funkcję za każdym razem, gdy zmienia się wprowadzony tekst:

      return (
        <div>
          <SearchBar
            query={searchQuery}
            onChange={setSearchQuery}   {/* passes the setter as a prop */}
          />
          <Results query={searchQuery} />
        </div>
      );
    }// Child receives the updater and calls it on user input
    function SearchBar({ query, onChange }) {
      return (
        <input
          value={query}
          onChange={e => onChange(e.target.value)}  {/* calls parent's setter */}
          placeholder="Search..."
        />
      );
    }
    

    Zapytanie znajduje się wyłącznie w App. SearchBar nic nie przechowuje; zgłasza każde naciśnięcie klawisza za pomocą właściwości onChange. Results otrzymuje zaktualizowane zapytanie jako właściwość i ponownie renderuje się. Dane przepływają w dół jako właściwości, a zdarzenia – w górę jako wywołania funkcji. (Komentarze wyjaśniające wewnątrz tagów otwierających służą wyłącznie do czytania; w rzeczywistym JSX komentarz umieszczany jest wśród dzieci elementu, a wewnątrz taga należy użyć zwykłego komentarza /* */ lub go pominąć.)

    Błędy, które zakłócają aktualizacje w React

    Mutacja stanu na miejscu

    Kuszące jest bezpośrednie modyfikowanie obiektu stanu i jego ponowne przekazanie:

    // WRONG — mutating state directly
    const [user, setUser] = useState({ name: "Priya", age: 28 });
    

    Pierwszy przykład birthday zmienia obiekt i przekazuje setterowi tę samą referencję, więc interfejs nie jest aktualizowany. Drugi przykład tworzy nowy obiekt za pomocą operatora spread, co React interpretuje jako zmianę:

    function birthday() {
      user.age = 29;        // ← directly modifying the object
      setUser(user);        // ← same object reference — React sees no change
    }                       //   UI does NOT update// CORRECT — create a new object
    function birthday() {
      setUser({ ...user, age: user.age + 1 }); // ← new object, React detects change
    }
    

    React decyduje, czy stan się zmienił, porównując referencje za pomocą Object.is, a nie sprawdzając ich zawartość. Jeśli zmienisz obiekt lub tablicę bezpośrednio i przekażesz tę samą referencję, React uzna, że nic się nie stało i może pominąć proces renderowania.

    Tablice podlegają tej samej zasadzie. Dodawanie elementów do istniejącej tablicy nie działa:

    // WRONG — mutating the array
    const [items, setItems] = useState([1, 2, 3]);
    items.push(4);       // ← modifies in place
    setItems(items);     // ← same reference — no re-render
    

    natomiast kopiowanie jej do nowej tablicy działa:

    // CORRECT — create a new array
    setItems([...items, 4]);  // ← spread creates a new array
    

    Mieszanie propów ze stanem

    Szybki test pomaga określić, co jest czym. Czy wartość pochodzi od komponentu nadrzędnego? To prop, który jest tylko do odczytu. Czy komponent sam go posiada i zmienia z czasem? To stan. Umieszczanie wartości, która się nigdy nie zmienia, w funkcji useState, jest oznaką pomieszania:

    // Wrong: using state for something that should be a prop
    function UserCard() {
      const [userName] = useState("Priya"); // ← why is this state? It never changes here
      return <p>{userName}</p>;
    }
    

    Jeśli dane są kontrolowane przez komponent nadrzędny, należy je przyjąć jako prop.

    // Correct: static data the parent controls comes as a prop
    function UserCard({ name }) {
      return <p>{name}</p>;
    }
    

    Przechowywanie wartości, które można obliczyć

    Nie wszystko, co się zmienia, potrzebuje własnego stanu. Przechowywanie licznika obok listy, którą on liczy, powoduje duplikację informacji:

    // Wrong: storing derived data in state
    const [items, setItems] = useState([...]);
    const [itemCount, setItemCount] = useState(0); // ← why is this state?
    

    Teraz każda zmiana w items wymaga odpowiedniej aktualizacji itemCount, a jedna zapomniana aktualizacja powoduje ich niespójność. Obliczanie tego wartości podczas renderowania automatycznie utrzymuje obie wartości zsynchronizowane:

    // Every time items changes, you have to remember to also update itemCount
    // And if you forget once, they're out of sync// Correct: derive it during render
    const [items, setItems] = useState([...]);
    const itemCount = items.length; // ← just a variable — always in sync
    

    Komponenty, które robią wszystko

    Jedna komponent, która renderuje pasek boczny, tabelę, formularz oraz kilka okien modalnych, jest trudna do odczytania, testowania i ponownego użycia. Praktyczna zasada: jeśli nie możesz opisać, co robi dana komponent, w jednym krótkim zdaniu, oznacza to, że robi zbyt wiele.

    // Hard to maintain — does everything
    function UserDashboard() {
      // manages user state
      // fetches orders
      // handles filters
      // manages pagination
      // controls modal open/close
      // renders sidebar
      // renders order table
      // renders filter controls
      // renders pagination
      // renders modal
      return ( /* 200 lines of JSX */ );
    }
    

    Rozdzielenie jej według zadań daje każdej części nazwę i konkretną funkcję:

    // Better — clear single responsibilities
    function UserDashboard() {
      return (
        <DashboardLayout>
          <OrderFilters />
          <OrderTable />
          <OrderPagination />
          <OrderDetailModal />
        </DashboardLayout>
      );
    }
    

    Projektowanie aplikacji z użyciem komponentów

    Narysuj granice przed napisaniem kodu

    Zacznij od projektu i oznacz poszczególne elementy. Wszystko, co się powtarza, stanowi komponent. Wszystko, co ma jedno jasne zadanie, również jest komponentem. Elementy, które zmieniają się razem, powinny być połączone; te, które zmieniają się niezależnie, należy rozdzielić. W przypadku strony z listą produktów projekt:

    Looking at the design:
      [ Filter Bar                          ]
      [ Product Card ][ Product Card ][ Product Card ]
      [ Product Card ][ Product Card ][ Product Card ]
      [ Pagination                          ]
    

    rozkłada się na następującą strukturę drzewiastą:

    Components:
      ProductPage
      ├── FilterBar
      │   ├── FilterGroup (×3)
      │   └── SortDropdown
      ├── ProductGrid
      │   └── ProductCard (×N)
      └── Pagination
    

    Umieść stan w najniższym wspólnym rodzicu

    Dla każdego elementu stanu znajdź w drzewie najniższy komponent, który go potrzebuje, i przechowuj tam ten stan:

    If only ProductCard needs the "expanded" state → put it in ProductCard
    If FilterBar and ProductGrid both need filters → put filters in ProductPage
    If Pagination and ProductGrid both need currentPage → put it in ProductPage
    

    Gdy dwa elementy siostrzane potrzebują tych samych danych, przenieś stan do ich najbliższego wspólnego rodzica i przekaż go dalej. Nazywa się to przenoszeniem stanu w górę drzewa. Przechowywanie stanu jak najniżej ogranicza również ilość elementów drzewa, które są ponownie renderowane po jego zmianie.

    Najpierw stwórz wersję statyczną, a potem dodaj stan

    Zacznij od danych ustawionych w sposób sztywny: bez useState, bez obsługi zdarzeń, tylko markup sterowany przez propsy. Gdy układ zostanie ustalony, określ, które wartości faktycznie zmieniają się z czasem, i wprowadź stan właśnie dla tych wartości. Pierwszym krokiem jest całkowicie statyczna karta:

    // Step 1: static — no state, hardcoded data
    function ProductCard() {
      return (
        <div className="card">
          <img src="/headphones.jpg" alt="Headphones" />
          <h3>Wireless Headphones</h3>
          <p>₹2,499</p>
          <button>Add to Cart</button>
        </div>
      );
    }
    

    Drugi krok zastępuje treść ustawioną sztywno propem product, a trzeci dodaje niewielki element stanu dla potwierdzenia „Dodano”, który sam się resetuje po dwóch sekundach:

    // Step 2: accept props — still no state
    function ProductCard({ product }) {
      return (
        <div className="card">
          <img src={product.image} alt={product.name} />
          <h3>{product.name}</h3>
          <p>₹{product.price.toLocaleString()}</p>
          <button>Add to Cart</button>
        </div>
      );
    }// Step 3: add state for interactivity
    function ProductCard({ product, onAddToCart }) {
      const [added, setAdded] = useState(false);  function handleAdd() {
        setAdded(true);
        onAddToCart(product.id);
        setTimeout(() => setAdded(false), 2000);
      }  return (
        <div className="card">
          <img src={product.image} alt={product.name} />
          <h3>{product.name}</h3>
          <p>₹{product.price.toLocaleString()}</p>
          <button onClick={handleAdd} disabled={added}>
            {added ? "Added ✓" : "Add to Cart"}
          </button>
        </div>
      );
    }
    

    Ponieważ struktura i interaktywność są oddzielone, każdy krok można łatwo sprawdzić osobno.

    Główne wnioski

    Cały model w skrócie. Dlaczego istnieje React:

    Why React:
      Plain JS DOM manipulation is hard to scale and maintain
      React: describe the UI, let React handle DOM updates
    

    oraz podstawy każdego z omówionych powyżej koncepcji, od JSX po częste błędy:

    JSX:
      HTML-like syntax in JavaScript — compiled to React.createElement()
      {} embeds any JavaScript expression
      Use className (not class), onClick (not onclick)Components:
      Functions that return JSX
      Capital letter names (Button, not button)
      Reusable, composable building blocksProps:
      Data passed from parent to child (like function arguments)
      Read-only — a component never modifies its own props
      Can be strings, numbers, objects, arrays, functionsState:
      const [value, setValue] = useState(initialValue)
      Data owned by a component that changes over time
      Calling the setter triggers a re-render
      Each component instance has its own stateRe-rendering:
      Happens when state changes, props change, or parent re-renders
      React diffs the virtual DOM and updates only what changed
      Not expensive — surgical DOM updatesDeclarative:
      Describe what the UI should look like
      React figures out what changed and how to update the DOM
      UI = f(state, props) — predictable, testableData flow:
      Props flow down (parent → child)
      Events flow up (child calls parent's function)
      One-way data flow keeps the application predictableCommon mistakes:
      Mutating state directly (use spread / new objects)
      Storing derived values in state (compute them during render)
      Overloading one component (split by responsibility)
    
    • Komponenty to funkcje, props to ich argumenty, a stan to ich pamięć. Interfejs użytkownika jest zawsze projekcją bieżącego stanu.
    • Ponowne renderowanie jest tanie ze względu na projekt; pracą w DOM, którą React za Ciebie wykonuje, jest ta droga część. Dobrze zorganizuj stan, zanim zaczniesz się martwić liczbą renderowań.
    • Zawsze przekazuj do funkcji ustawiających nowe obiekty i tablice; React porównuje referencje, a nie zawartość.
    • Zachowuj stan w najniższym komponencie, który go potrzebuje, wyprowadzaj wszystko inne podczas renderowania i pozwól, by zdarzenia przepływały w górę poprzez props typu callback.

    Kontekst, efekty, niestandardowe hooki oraz optymalizacja wydajności opierają się na tych koncepcjach. Gdy dobrze opanujesz komponenty, props, stan i ponowne renderowanie, bardziej zaawansowane aspekty Reacta staną się rozwinięciem modelu, który już rozumiesz, a nie nowymi zasadami do zapamiętania.

    Literatura pokrewna

  • Redux Toolkit od First Slice do danych asynchronicznych: Pracujący model mentalny — Dowiedz się, jak sklepa Redux Toolkit, jego komponenty typu slices, reduktory oparte na Immer, selektory oraz createAsyncThunk współpracują ze sobą, przy użyciu licznika, koszyka zakupowego i listy produktów.