Lekcja 20: Zarządzanie Stanem Globalnym (Redux, Zustand) - Wprowadzenie

W miarę jak aplikacje React rosną, zarządzanie stanem może stać się coraz bardziej skomplikowane. Wbudowane mechanizmy Reacta, takie jak podnoszenie stanu, useReducer i Context API, są wystarczające dla wielu przypadków, ale w dużych aplikacjach z wieloma współdzielonymi i często aktualizowanymi danymi, mogą prowadzić do problemów z wydajnością (Context API) lub skomplikowanej logiki przekazywania stanu i funkcji (podnoszenie stanu, useReducer + Context).

W takich sytuacjach często sięga się po zewnętrzne biblioteki do zarządzania stanem globalnym. Zapewniają one scentralizowane miejsce do przechowywania stanu aplikacji, przewidywalne wzorce jego aktualizacji oraz narzędzia ułatwiające debugowanie i organizację kodu. W tej lekcji wprowadzimy dwie popularne biblioteki: Redux (z Redux Toolkit) i Zustand.

Problem: Skalowalność Zarządzania Stanem w React

Redux i Redux Toolkit

Redux to jedna z najstarszych i najpopularniejszych bibliotek do zarządzania stanem w aplikacjach JavaScript (nie tylko React). Opiera się na trzech fundamentalnych zasadach:

  1. Jedno źródło prawdy: Stan całej aplikacji jest przechowywany w jednym obiekcie (store) wewnątrz drzewa stanu.
  2. Stan jest tylko do odczytu: Jedynym sposobem na zmianę stanu jest wysłanie (dispatch) akcji – obiektu opisującego, co się wydarzyło.
  3. Zmiany są dokonywane za pomocą czystych funkcji: Aby określić, jak stan zmienia się w odpowiedzi na akcję, piszemy czyste funkcje zwane reduktorami (reducers).

Chociaż Redux jest potężny, jego "czysta" implementacja wymagała pisania sporej ilości kodu szablonowego (boilerplate). Aby to uprościć, zespół Reduxa stworzył Redux Toolkit (RTK) – oficjalny, rekomendowany zestaw narzędzi do efektywnego rozwoju z Reduxem.

Kluczowe elementy Redux Toolkit:

Przykład (bardzo uproszczony) z Redux Toolkit:

// features/counter/counterSlice.js
import { createSlice } from \"@reduxjs/toolkit\";

const initialState = { value: 0 };

const counterSlice = createSlice({
  name: \"counter\",
  initialState,
  reducers: {
    increment: (state) => {
      // RTK używa Immer pod spodem, co pozwala \"mutować\" stan w reduktorach
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
});

// Eksportujemy kreatory akcji i reduktor
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;

// app/store.js
import { configureStore } from \"@reduxjs/toolkit\";
import counterReducer from \"../features/counter/counterSlice\";

export const store = configureStore({
  reducer: {
    counter: counterReducer, // Łączymy reduktory
    // ... inne reduktory
  },
});

// index.js
import React from \"react\";
import ReactDOM from \"react-dom/client\";
import { Provider } from \"react-redux\";
import { store } from \"./app/store\";
import App from \"./App\";

const root = ReactDOM.createRoot(document.getElementById(\"root\"));
root.render(
  <Provider store={store}> { /* Udostępniamy store aplikacji */ }
    <App />
  </Provider>
);

// features/counter/Counter.js
import React from \"react\";
import { useSelector, useDispatch } from \"react-redux\";
import { increment, decrement } from \"./counterSlice\";

function Counter() {
  // Odczytujemy stan za pomocą useSelector
  const count = useSelector((state) => state.counter.value);
  // Pobieramy funkcję dispatch
  const dispatch = useDispatch();

  return (
    <div>
      <p>Licznik (Redux): {count}</p>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  );
}

Zustand

Zustand to nowsza, minimalistyczna biblioteka do zarządzania stanem, która zyskała dużą popularność dzięki swojej prostocie i mniejszej ilości kodu szablonowego w porównaniu do Reduxa (nawet z RTK).

Kluczowe cechy Zustand:

Przykład (bardzo uproszczony) z Zustand:

// store/counterStore.js
import { create } from \"zustand\";

// Tworzymy store za pomocą funkcji create
const useCounterStore = create((set) => ({
  // Stan początkowy
  count: 0,
  // Funkcje do aktualizacji stanu (odpowiedniki akcji/reduktorów)
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

export default useCounterStore;

// components/CounterZustand.js
import React from \"react\";
import useCounterStore from \"../store/counterStore\";

function CounterZustand() {
  // Używamy Hooka zwróconego przez create
  // Możemy selekcjonować tylko potrzebne części stanu i akcje
  const count = useCounterStore((state) => state.count);
  const increment = useCounterStore((state) => state.increment);
  const decrement = useCounterStore((state) => state.decrement);

  // Lub pobrać cały store:
  // const { count, increment, decrement } = useCounterStore();

  return (
    <div>
      <p>Licznik (Zustand): {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

export default CounterZustand;

// W App.jsx wystarczy użyć komponentu CounterZustand
// Nie ma potrzeby owijania aplikacji w Providera (chyba że używamy Contextu Zustand)

Redux vs Zustand - Kiedy co wybrać?

| Cecha | Redux (z Redux Toolkit) | Zustand | | :-------------------- | :------------------------------------------ | :-------------------------------------------- | | Złożoność / Boilerplate | Średnia (znacznie mniej niż czysty Redux) | Niska | | Rozmiar biblioteki | Większy | Mniejszy | | Ekosystem / Narzędzia | Bardzo duży (DevTools, middleware itp.) | Mniejszy, ale rosnący (obsługuje DevTools) | | Elastyczność | Strukturalny (slice, reduktory, akcje) | Bardzo elastyczny (proste funkcje) | | Krzywa uczenia | Średnia | Niska | | Opinie społeczności | Standard w wielu dużych projektach | Zyskuje na popularności, chwalony za prostotę | | Przypadki użycia | Duże aplikacje, złożony stan globalny, potrzeba rozbudowanych narzędzi | Mniejsze i średnie aplikacje, prostszy stan globalny, szybkość rozwoju | **Podsumowując:**

Wybór zależy od specyfiki projektu, rozmiaru zespołu i preferencji deweloperów.


Ćwiczenie praktyczne

Cel: Zrozumieć podstawową ideę selektorów i dispatchowania akcji w obu bibliotekach (bez pełnej implementacji).

Zadanie:

  1. Przeanalizuj przykłady kodu dla Redux Toolkit i Zustand w tej lekcji.
  2. Zwróć uwagę, jak w obu przypadkach odczytywany jest stan w komponencie (useSelector w Redux, selektor w Hooku Zustand).
  3. Zwróć uwagę, jak wywoływane są zmiany stanu (dispatch(actionCreator()) w Redux, wywołanie funkcji ze store\"u w Zustand).
  4. Zastanów się, która składnia wydaje Ci się bardziej intuicyjna na pierwszy rzut oka.

To ćwiczenie ma charakter teoretyczny i ma na celu zapoznanie się z podstawowymi koncepcjami. Pełna implementacja wymagałaby konfiguracji projektu.


Zadanie do samodzielnego wykonania

Jeśli chcesz zgłębić temat:

  1. Wybierz jedną z bibliotek (Redux Toolkit lub Zustand).
  2. Przejrzyj oficjalny tutorial "Quick Start" dla wybranej biblioteki.
  3. Spróbuj zaimplementować prosty licznik (jak w przykładach) lub listę zadań (Todo List) w nowym projekcie React, używając wybranej biblioteki do zarządzania stanem.
  4. Skonfiguruj Redux DevTools (działa z obiema bibliotekami przy odpowiedniej konfiguracji) i zobacz, jak możesz śledzić zmiany stanu i akcje.

FAQ - Zarządzanie Stanem Globalnym (Redux, Zustand)

Czy zawsze potrzebuję zewnętrznej biblioteki do zarządzania stanem?

Nie. Dla wielu aplikacji, zwłaszcza mniejszych, wbudowane mechanizmy Reacta (, , Context API, podnoszenie stanu) są w zupełności wystarczające. Wprowadzanie zewnętrznej biblioteki dodaje złożoność, więc warto to robić tylko wtedy, gdy faktycznie rozwiązuje ona konkretne problemy skalowalności lub organizacji stanu.

Czy Redux jest przestarzały?

Nie. Chociaż pojawiły się nowsze alternatywy, Redux (zwłaszcza z Redux Toolkit) jest nadal bardzo popularny, aktywnie rozwijany i używany w wielu dużych, produkcyjnych aplikacjach. Jego dojrzałość i bogaty ekosystem są jego dużymi zaletami.

Czy Zustand używa Context API pod spodem?

Domyślnie nie. Zustand przechowuje stan poza drzewem komponentów React i używa subskrypcji do informowania komponentów o zmianach. Istnieje jednak możliwość użycia Zustand w połączeniu z Context API, jeśli chcemy ograniczyć zasięg store\"u do określonego poddrzewa.

Jak Redux i Zustand radzą sobie z asynchronicznością?

Redux Toolkit dostarcza i middleware (Redux Thunk domyślnie) do obsługi operacji asynchronicznych. W Zustand można po prostu używać funkcji wewnątrz funkcji aktualizujących stan, ponieważ nie ma ścisłego wymogu czystości jak w reduktorach Reduxa.

Co to jest Immer i dlaczego Redux Toolkit go używa?

Immer to biblioteka, która pozwala pisać kod mutujący obiekty i tablice w sposób bardziej naturalny, a pod spodem dba o to, aby faktycznie stworzyć nową, zaktualizowaną kopię (zachowując niemutowalność). Redux Toolkit używa Immer w , co pozwala pisać reduktory w stylu `state.value += 1` zamiast `return { ...state, value: state.value + 1 }`.

Czy mogę używać Redux DevTools z Zustand?

Tak, Zustand oferuje middleware, które pozwala na integrację z Redux DevTools, co umożliwia inspekcję stanu i podróżowanie w czasie (time-travel debugging), podobnie jak w Reduxie.

Jakie są inne popularne biblioteki do zarządzania stanem w React?

Oprócz Reduxa i Zustanda, popularne są również Jotai, Recoil (eksperymentalny od Facebooka/Meta), MobX. Każda z nich ma nieco inne podejście, API i filozofię działania.

Czy mogę używać wielu bibliotek do zarządzania stanem w jednej aplikacji?

Technicznie jest to możliwe, ale zazwyczaj niezalecane, ponieważ może prowadzić do niepotrzebnej złożoności i trudności w zarządzaniu przepływem danych. Lepiej wybrać jedno główne rozwiązanie dla stanu globalnego i ewentualnie uzupełniać je stanem lokalnym komponentów lub Context API tam, gdzie to ma sens.