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
- Prop Drilling: Przekazywanie stanu przez wiele poziomów komponentów, które go nie używają.
- Problemy z wydajnością Context API: Rerender wszystkich konsumentów przy każdej zmianie wartości kontekstu, nawet jeśli interesuje ich tylko część stanu.
- Rozproszona logika biznesowa: Logika aktualizacji stanu może być rozproszona po wielu komponentach lub niestandardowych Hookach.
- Trudności w debugowaniu: Śledzenie przepływu danych i przyczyn zmian stanu w dużej aplikacji może być wyzwaniem.
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:
- Jedno źródło prawdy: Stan całej aplikacji jest przechowywany w jednym obiekcie (store) wewnątrz drzewa stanu.
- Stan jest tylko do odczytu: Jedynym sposobem na zmianę stanu jest wysłanie (dispatch) akcji – obiektu opisującego, co się wydarzyło.
- 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:
configureStore
: Upraszcza tworzenie i konfigurację store Reduxa, automatycznie dodając przydatne middleware (jak Redux Thunk do obsługi asynchronicznych akcji) i narzędzia deweloperskie (Redux DevTools).createSlice
: Pozwala zdefiniować "fragment" stanu (slice) wraz z jego reduktorami i automatycznie generuje kreatory akcji (action creators) i typy akcji. Znacząco redukuje boilerplate.createAsyncThunk
: Ułatwia obsługę typowych asynchronicznych przepływów (np. pobieranie danych z API) z dispatchowaniem akcji pending/fulfilled/rejected.- Integracja z React (
react-redux
): Bibliotekareact-redux
dostarcza Hooki do interakcji ze store Reduxa w komponentach React: <Provider store={store}>
: Komponent do owinięcia aplikacji, udostępniający store Reduxa.useSelector
: Hook do odczytywania danych ze stanu Reduxa w komponencie. Pozwala subskrybować tylko wybrane fragmenty stanu.useDispatch
: Hook do uzyskania dostępu do funkcjidispatch
store Reduxa, aby wysyłać akcje.
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:
- Minimalizm: Małe API, łatwe do nauczenia.
- Brak boilerplate: Nie wymaga definiowania akcji, kreatorów akcji czy reduktorów w tradycyjny sposób. Stan jest aktualizowany przez proste funkcje.
- Opiera się na Hookach: Głównym sposobem interakcji jest Hook zwracany przez funkcję tworzącą store.
- Elastyczność: Pozwala na łatwe tworzenie wielu niezależnych store\"ów.
- Wydajność: Subskrybuje komponenty tylko do tych fragmentów stanu, których używają, co pomaga unikać niepotrzebnych rerenderów.
- Opcjonalne middleware: Obsługuje middleware do dodawania funkcjonalności (np. Redux DevTools, persystencja).
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:**- Redux (z RTK) jest dojrzałym, potężnym rozwiązaniem, świetnym dla dużych aplikacji z złożonym stanem i potrzebą rozbudowanych narzędzi deweloperskich. Wymaga nieco więcej nauki i kodu.
- Zustand jest prostszą, bardziej elastyczną alternatywą, idealną dla mniejszych i średnich projektów lub gdy priorytetem jest szybkość rozwoju i minimalizm.
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:
- Przeanalizuj przykłady kodu dla Redux Toolkit i Zustand w tej lekcji.
- Zwróć uwagę, jak w obu przypadkach odczytywany jest stan w komponencie (
useSelector
w Redux, selektor w Hooku Zustand). - Zwróć uwagę, jak wywoływane są zmiany stanu (
dispatch(actionCreator())
w Redux, wywołanie funkcji ze store\"u w Zustand). - 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:
- Wybierz jedną z bibliotek (Redux Toolkit lub Zustand).
- Przejrzyj oficjalny tutorial "Quick Start" dla wybranej biblioteki.
- 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.
- 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.