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.
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.
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 rozwiązuje prop drilling bez dodatkowych bibliotek. Poznałeś go wcześniej w tym module. Jego główne zalety i ograniczenia:
Zalety:
createContext, Provider, useContextOgraniczenia:
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 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?
Kiedy NIE Redux?
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.
| 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 |
Na Stacji Jupiter każdy system ma swoje przeznaczenie. Podobnie z zarządzaniem stanem:
useState) - dane potrzebne tylko jednemu komponentowi lub jego bezpośrednim dzieciomPamię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: