Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Wzorce zarządzania stanem

Na Stacji Jupiter zarządzasz wieloma systemami jednocześnie - nawigacją, paliwem, załogą, komunikacją. Każdy system potrzebuje danych i musi je udostępniać innym modułom. W React zarządzanie stanem to jedno z najważniejszych wyzwań architektonicznych. Wybór odpowiedniego wzorca determinuje, czy Twoja aplikacja będzie łatwa w utrzymaniu i skalowalna, czy stanie się chaotycznym labiryntem zależności.

Problem: Prop drilling

Zanim poznałeś Context API, jedynym sposobem na przekazywanie danych w dół drzewa komponentów było

prop drilling
- ręczne przekazywanie propsów przez każdy poziom. To jak przesyłanie wiadomości przez łańcuch członków załogi, gdzie każdy musi powtórzyć komunikat dalej, choć sam go nie potrzebuje.

1// Prop drilling - 4 poziomy przekazywania!
2function SpaceStation({ missionData }) {
3  return <Deck missionData={missionData} />;
4}
5
6function Deck({ missionData }) {
7  // Deck nie używa missionData - tylko przekazuje dalej
8  return <Room missionData={missionData} />;
9}
10
11function Room({ missionData }) {
12  // Room też nie używa - przekazuje dalej
13  return <Terminal missionData={missionData} />;
14}
15
16function Terminal({ missionData }) {
17  // Dopiero tutaj dane są potrzebne!
18  return <p>Misja: {missionData.name}</p>;
19}

Przy 4 poziomach to jeszcze znośne, ale przy 8-10 poziomach (co w dużych aplikacjach jest normą) kod staje się nieczytelny, a każda zmiana w strukturze danych wymaga modyfikacji wielu pośrednich komponentów.

State colocation — trzymaj stan blisko miejsca użycia

Zanim sięgniesz po globalne rozwiązania, rozważ state colocation - zasadę trzymania stanu jak najbliżej komponentów, które go potrzebują. Nie każda dana musi być globalna. To jak na statku kosmicznym: temperatura kabiny pilota nie musi być znana w maszynowni.

1// ZŁE: Globalny stan dla lokalnych danych
2function App() {
3  const [searchQuery, setSearchQuery] = useState('');
4  const [isModalOpen, setIsModalOpen] = useState(false);
5  const [formData, setFormData] = useState({});
6  // ... 20 innych stanów
7
8  return (
9    <GlobalContext.Provider value={/* wszystko */}>
10      <Dashboard />
11    </GlobalContext.Provider>
12  );
13}
14
15// DOBRE: Stan blisko miejsca użycia
16function App() {
17  // Tylko naprawdę globalne dane
18  const [user, setUser] = useState(null);
19  const [theme, setTheme] = useState('dark');
20
21  return (
22    <AppContext.Provider value={{ user, theme }}>
23      <Dashboard />
24    </AppContext.Provider>
25  );
26}
27
28function SearchPanel() {
29  // Lokalny stan - tylko ten komponent go potrzebuje
30  const [searchQuery, setSearchQuery] = useState('');
31  return <input value={searchQuery} onChange={e => setSearchQuery(e.target.value)} />;
32}

Reguła kciuka: jeśli stan jest używany tylko przez jeden komponent lub jego bezpośrednie dzieci - trzymaj go lokalnie. Przenoś do góry (lifting state up) lub do kontekstu dopiero, gdy potrzebują go odległe gałęzie drzewa komponentów.

Context API — wbudowane rozwiązanie

Context API rozwiązuje prop drilling bez dodatkowych bibliotek. Poznałeś go wcześniej w tym module. Jego główne zalety i ograniczenia:

Zalety:

  • Wbudowany w React - zero dodatkowych zależności
  • Prosty API:
    createContext
    ,
    Provider
    ,
    useContext
  • Doskonały dla danych zmieniających się rzadko (theme, auth, locale)

Ograniczenia:

  • Każda zmiana wartości re-renderuje WSZYSTKICH konsumentów
  • Brak wbudowanego cache'owania danych z API
  • Przy złożonym stanie wymaga łączenia z useReducer
  • Trudna optymalizacja wydajności w dużych aplikacjach
1// Context API - dobry wybór dla konfiguracji statku
2const ShipConfigContext = createContext();
3
4function ShipConfigProvider({ children }) {
5  const [config, setConfig] = useState({
6    theme: 'dark-nebula',
7    language: 'pl',
8    soundEnabled: true,
9  });
10
11  return (
12    <ShipConfigContext.Provider value={{ config, setConfig }}>
13      {children}
14    </ShipConfigContext.Provider>
15  );
16}

Redux — centrum dowodzenia dla dużych flot

Redux to sprawdzony wzorzec zarządzania stanem, inspirowany architekturą Flux. Działa jak centralne centrum dowodzenia floty - jeden store (magazyn) przechowuje cały stan aplikacji, a zmiany odbywają się wyłącznie przez dispatch akcji. Nowoczesny Redux piszemy z użyciem Redux Toolkit -

createSlice
definiuje stan i reducery, a
configureStore
buduje magazyn:

1// Redux Toolkit - nowoczesny standard Redux
2import { createSlice, configureStore } from '@reduxjs/toolkit';
3
4// Slice = fragment stanu + reducery + akcje w jednym miejscu
5const missionSlice = createSlice({
6  name: 'mission',
7  initialState: {
8    crew: [],
9    fuel: 100,
10    destination: null,
11    status: 'docked',
12  },
13  reducers: {
14    // Immer pozwala "mutować" stan - RTK zamienia to na bezpieczną aktualizację
15    addCrewMember: (state, action) => {
16      state.crew.push(action.payload);
17    },
18    setDestination: (state, action) => {
19      state.destination = action.payload;
20    },
21    burnFuel: (state, action) => {
22      state.fuel = Math.max(0, state.fuel - action.payload);
23    },
24    launch: (state) => {
25      state.status = 'in-flight';
26    },
27  },
28});
29
30// createSlice automatycznie generuje kreatory akcji
31export const { addCrewMember, setDestination, burnFuel, launch } = missionSlice.actions;
32
33// configureStore buduje magazyn (z DevTools i middleware w komplecie)
34const store = configureStore({
35  reducer: {
36    mission: missionSlice.reducer,
37  },
38});
39
40// Użycie w komponencie:
41function MissionControl() {
42  const dispatch = useDispatch();
43  const { crew, fuel, status } = useSelector(state => state.mission);
44
45  return (
46    <div>
47      <p>Status: {status} | Paliwo: {fuel}% | Załoga: {crew.length}</p>
48      <button onClick={() => dispatch(launch())}>Start!</button>
49      <button onClick={() => dispatch(burnFuel(10))}>Spal paliwo</button>
50    </div>
51  );
52}

Kiedy Redux?

  • Aplikacja z wieloma developerami (jasne reguły, przewidywalny przepływ danych)
  • Złożony stan z wieloma powiązanymi akcjami
  • Potrzebujesz DevTools z time-travel debugging
  • Middleware do obsługi side-effects (thunki, saga)

Kiedy NIE Redux?

  • Prosta aplikacja z kilkoma stanami
  • Mały zespół lub solo developer
  • Głównie dane z API (lepiej React Query)

Zustand — lekki silnik manewrowy

Zustand to minimalistyczna biblioteka do zarządzania stanem. Jeśli Redux to potężny silnik główny, Zustand to zwinny silnik manewrowy - robi to samo, ale z mniejszym narzutem. Bez providerów, bez boilerplate'u, bez problemu re-renderów Context API.

1// Zustand - prosty store bez providerów
2// import { create } from 'zustand';
3
4// Tworzenie store - to wszystko co potrzebujesz!
5// const useShipStore = create((set) => ({
6//   fuel: 100,
7//   position: { x: 0, y: 0 },
8//   crew: 5,
9//
10//   burnFuel: (amount) => set((state) => ({
11//     fuel: Math.max(0, state.fuel - amount)
12//   })),
13//
14//   moveTo: (x, y) => set({ position: { x, y } }),
15//
16//   addCrew: () => set((state) => ({
17//     crew: state.crew + 1
18//   })),
19// }));
20
21// Użycie w komponencie - bez Providera!
22function FuelGauge() {
23  // Komponent re-renderuje się TYLKO gdy zmieni się fuel
24  // const fuel = useShipStore((state) => state.fuel);
25  // return <div>Paliwo: {fuel}%</div>;
26}
27
28function NavigationPanel() {
29  // Odczytuje TYLKO position - zmiana fuel nie powoduje re-renderu
30  // const position = useShipStore((state) => state.position);
31  // const moveTo = useShipStore((state) => state.moveTo);
32  // return <button onClick={() => moveTo(10, 20)}>Leć!</button>;
33}

Zustand automatycznie optymalizuje re-rendery dzięki selektorom. Każdy komponent subskrybuje się tylko na te dane, których faktycznie używa. Zmiana paliwa nie re-renderuje panelu nawigacji - i odwrotnie.

Porównanie wzorców — mapa galaktyki

| Cecha | Context API | Redux Toolkit | Zustand | |-------|------------|---------------|---------| | Rozmiar | 0 KB (wbudowany) | ~11 KB | ~1.5 KB | | Boilerplate | Średni | Duży (ale RTK pomaga) | Minimalny | | Re-rendery | Wszyscy konsumenci | Zoptymalizowane (selektory) | Zoptymalizowane (selektory) | | DevTools | Brak dedykowanych | Rozbudowane (time-travel) | Rozszerzenie Redux DevTools | | Krzywa nauki | Łatwa | Stroma | Łatwa | | Najlepsze dla | Theme, auth, locale | Duże aplikacje, duże zespoły | Większość aplikacji |

Zasada wyboru narzędzia

Na Stacji Jupiter każdy system ma swoje przeznaczenie. Podobnie z zarządzaniem stanem:

  1. Stan lokalny (
    useState
    )
    - dane potrzebne tylko jednemu komponentowi lub jego bezpośrednim dzieciom
  2. Lifted state (stan podniesiony) - dane współdzielone przez 2-3 bliskie komponenty, przenieś do ich wspólnego rodzica
  3. Context API - dane globalne zmieniające się rzadko: theme, auth, locale, język
  4. Zustand - stan globalny w małych/średnich aplikacjach, gdy potrzebujesz wydajności i prostoty
  5. Redux Toolkit - duże aplikacje z wieloma developerami, złożona logika biznesowa, potrzeba DevTools

Pamiętaj: nie ma jednego najlepszego rozwiązania. Najlepsi kosmiczni inżynierowie dobierają narzędzie do problemu, a nie problem do narzędzia.

Przetestuj porównanie wzorców w edytorze poniżej:

Ir a CodeWorlds