Kurs JavaScript i React · Moduł 4: Context API i useReducer
Optymalizacja Context API
W tej lekcji6
Centrum dowodzenia misji obsługuje setki sygnałów na sekundę. Gdyby każdy sygnał powodował restart wszystkich systemów - statek by nie dolecieł na orbitę. Podobnie działa React: gdy wartość kontekstu się zmienia, każdy komponent korzystający z tego kontekstu jest przerenderowywany. W małych aplikacjach to niewidoczne, ale w złożonych systemach może znacznie spowolnić działanie.
Problem: nadmierne renderowanie z kontekstem
Rozważmy prosty przykład. Mamy jeden kontekst przechowujący cały stan statku:
1const ShipContext = createContext();
2
3function ShipProvider({ children }) {
4 const [state, dispatch] = useReducer(shipReducer, {
5 status: 'docked',
6 fuel: 100,
7 crewCount: 5,
8 alerts: []
9 });
10
11 return (
12 <ShipContext.Provider value={{ state, dispatch }}>
13 {children}
14 </ShipContext.Provider>
15 );
16}
17
18function FuelGauge() {
19 const { state } = useContext(ShipContext);
20 console.log('FuelGauge renderuje się');
21 return <p>Paliwo: {state.fuel}%</p>;
22}
23
24function AlertBell() {
25 const { state } = useContext(ShipContext);
26 console.log('AlertBell renderuje się');
27 return <p>Alerty: {state.alerts.length}</p>;
28}Gdy zmienia się alerts (np. nowy alert), FuelGauge też się rerenderuje - mimo że poziom paliwa się nie zmienił. Każda zmiana dowolnego pola w state wyzwala renderowanie wszystkich konsumentów kontekstu.
Rozwiązanie 1: Podział kontekstu na stan i dispatch
Najprostszą i najskuteczniejszą optymalizacją jest rozdzielenie kontekstu na dwa osobne: jeden dla stanu (który zmienia się często), drugi dla dispatch (który jest stały przez cały czas życia komponentu):
1const ShipStateContext = createContext();
2const ShipDispatchContext = createContext();
3
4function ShipProvider({ children }) {
5 const [state, dispatch] = useReducer(shipReducer, initialState);
6
7 return (
8 <ShipDispatchContext.Provider value={dispatch}>
9 <ShipStateContext.Provider value={state}>
10 {children}
11 </ShipStateContext.Provider>
12 </ShipDispatchContext.Provider>
13 );
14}
15
16// Dedykowane hooki dla każdego kontekstu
17function useShipState() {
18 return useContext(ShipStateContext);
19}
20
21function useShipDispatch() {
22 return useContext(ShipDispatchContext);
23}Dlaczego to działa? dispatch zwracany przez useReducer jest stabilną referencją - React gwarantuje, że nie zmienia się między renderowaniami. Komponenty, które tylko wysyłają akcje (i nie wyświetlają stanu), nie będą rerenderowane gdy stan się zmieni:
1// Ten komponent rerenderuje się tylko gdy zmienia się stan
2function FuelGauge() {
3 const state = useShipState();
4 return <p>Paliwo: {state.fuel}%</p>;
5}
6
7// Ten komponent NIE rerenderuje się gdy zmienia się stan statku
8// (bo korzysta tylko z dispatch, a dispatch jest stabilny)
9function LaunchButton() {
10 const dispatch = useShipDispatch();
11 return (
12 <button onClick={() => dispatch({ type: 'LAUNCH' })}>
13 Start
14 </button>
15 );
16}Rozwiązanie 2: Podział na wiele wyspecjalizowanych kontekstów
Zamiast jednego kontekstu dla całego stanu, możemy tworzyć osobne konteksty dla różnych obszarów funkcjonalnych. Komponenty subskrybują tylko ten kontekst, który ich dotyczy:
1// Osobny kontekst dla paliwa
2const FuelContext = createContext();
3
4function FuelProvider({ children }) {
5 const [fuel, setFuel] = useState(100);
6 const refuel = () => setFuel(100);
7 const consumeFuel = (amount) => setFuel(prev => Math.max(0, prev - amount));
8
9 return (
10 <FuelContext.Provider value={{ fuel, refuel, consumeFuel }}>
11 {children}
12 </FuelContext.Provider>
13 );
14}
15
16// Osobny kontekst dla alertów
17const AlertContext = createContext();
18
19function AlertProvider({ children }) {
20 const [alerts, setAlerts] = useState([]);
21 const addAlert = (msg) => setAlerts(prev => [...prev, { id: Date.now(), msg }]);
22 const dismissAlert = (id) => setAlerts(prev => prev.filter(a => a.id !== id));
23
24 return (
25 <AlertContext.Provider value={{ alerts, addAlert, dismissAlert }}>
26 {children}
27 </AlertContext.Provider>
28 );
29}
30
31// FuelGauge rerenderuje się tylko gdy zmienia się paliwo
32function FuelGauge() {
33 const { fuel } = useContext(FuelContext);
34 return <p>Paliwo: {fuel}%</p>;
35}
36
37// AlertBell rerenderuje się tylko gdy zmieniają się alerty
38function AlertBell() {
39 const { alerts } = useContext(AlertContext);
40 return <p>Alerty: {alerts.length}</p>;
41}Rozwiązanie 3: React.memo dla komponentów potomnych
React.memo to HOC (Higher-Order Component), który opakowuje komponent i sprawia, że rerenderuje się tylko wtedy, gdy jego props się zmieniły. To szczególnie przydatne dla komponentów-dzieci providerów:
1// Bez memo - rerenderuje się za każdym razem, gdy rodzic się rerenderuje
2function CrewMember({ name, role }) {
3 console.log(`Renderuję: ${name}`);
4 return <li>{name} - {role}</li>;
5}
6
7// Z memo - rerenderuje się tylko gdy name lub role się zmienią
8const CrewMemberOptimized = React.memo(function CrewMember({ name, role }) {
9 console.log(`Renderuję: ${name}`);
10 return <li>{name} - {role}</li>;
11});
12
13function CrewList() {
14 const { state } = useShipState();
15 return (
16 <ul>
17 {state.crew.map(member => (
18 // CrewMemberOptimized nie rerenderuje się gdy zmienia się paliwo lub status
19 <CrewMemberOptimized
20 key={member.id}
21 name={member.name}
22 role={member.role}
23 />
24 ))}
25 </ul>
26 );
27}Kiedy Context, a kiedy props?
Nie każde dane powinny trafić do kontekstu. Oto praktyczny przewodnik decyzyjny:
Używaj Context gdy:
- Dane są potrzebne w wielu komponentach na różnych poziomach drzewa
- Unikasz przekazywania props przez więcej niż 2-3 poziomy (prop drilling)
- Dane zmieniają się rzadko lub dotyczą szerokiego kontekstu aplikacji (motyw, język, dane użytkownika)
1// Dobre kandydaty do Context
2const ThemeContext = createContext(); // motyw (ciemny/jasny)
3const AuthContext = createContext(); // zalogowany użytkownik
4const LanguageContext = createContext(); // język interfejsuUżywaj props gdy:
- Dane przepływają tylko przez 1-2 poziomy komponentów
- Komponent jest wielokrotnie używany w różnych kontekstach (reużywalny)
- Dane są specyficzne dla konkretnej sekcji UI
1// Lepiej jako props - to dane specyficzne dla komponentu
2function MissionCard({ mission, onSelect, isActive }) {
3 return (
4 <div className={isActive ? 'active' : ''} onClick={() => onSelect(mission)}>
5 <h3>{mission.name}</h3>
6 <p>{mission.status}</p>
7 </div>
8 );
9}Praktyczna zasada
Zanim dodasz coś do kontekstu, zadaj sobie pytanie: "Czy naprawdę potrzebuję tego globalnie, czy to tylko prop drilling, który mogę rozwiązać lepszą strukturą komponentów?"
1// Czasem problem prop drilling można rozwiązać przez composition
2// zamiast Context:
3
4// Zamiast przekazywać onSelectMission przez 3 poziomy...
5function MissionControl() {
6 const [selected, setSelected] = useState(null);
7
8 return (
9 // Przekaż gotowy komponent jako children, zamiast funkcji jako prop
10 <MissionList
11 renderItem={(mission) => (
12 <MissionCard
13 mission={mission}
14 isActive={selected?.id === mission.id}
15 onSelect={setSelected}
16 />
17 )}
18 />
19 );
20}Optymalizacja kontekstu to balans między wygodą a wydajnością. W większości aplikacji podstawowy podział na kontekst stanu i kontekst dispatch wystarczy. Bardziej zaawansowane techniki warto stosować, gdy faktycznie zaobserwujesz problemy z wydajnością.
Kod do tej lekcji: App.jsx
1import React, { createContext, useContext, useReducer, memo } from 'react';
2
3// --- Osobne konteksty dla stanu i dispatch ---
4// Optymalizacja: komponenty które tylko dispatchują nie re-renderują się przy zmianie stanu
5const StateContext = createContext();
6const DispatchContext = createContext();
7
8const initialState = {
9 ships: [
10 { id: 1, name: 'Apollo VII', fuel: 80, active: true },
11 { id: 2, name: 'Orion II', fuel: 45, active: false },
12 { id: 3, name: 'Hermes', fuel: 100, active: true },
13 ],
14 selectedId: null,
15};
16
17function reducer(state, action) {
18 switch (action.type) {
19 case 'SELECT_SHIP':
20 return { ...state, selectedId: action.id };
21 case 'REFUEL':
22 return {
23 ...state,
24 ships: state.ships.map(s =>
25 s.id === action.id ? { ...s, fuel: Math.min(100, s.fuel + 20) } : s
26 ),
27 };
28 case 'TOGGLE_ACTIVE':
29 return {
30 ...state,
31 ships: state.ships.map(s =>
32 s.id === action.id ? { ...s, active: !s.active } : s
33 ),
34 };
35 default:
36 return state;
37 }
38}
39
40function FleetProvider({ children }) {
41 const [state, dispatch] = useReducer(reducer, initialState);
42 return (
43 <StateContext.Provider value={state}>
44 <DispatchContext.Provider value={dispatch}>
45 {children}
46 </DispatchContext.Provider>
47 </StateContext.Provider>
48 );
49}
50
51// Hooki dostępu do kontekstów
52function useFleetState() { return useContext(StateContext); }
53function useFleetDispatch() { return useContext(DispatchContext); }
54
55// --- Komponenty zoptymalizowane z React.memo ---
56
57// Ten komponent re-renderuje się TYLKO gdy zmienia się konkretny statek
58const ShipCard = memo(function ShipCard({ ship }) {
59 const dispatch = useFleetDispatch();
60 console.log(`[RENDER] ShipCard: ${ship.name}`);
61 return (
62 <div style={{ ...cardStyle, border: ship.active ? '1px solid #4fc3f7' : '1px solid #333' }}>
63 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
64 <span style={{ color: ship.active ? '#4fc3f7' : '#888', fontWeight: 'bold' }}>
65 {ship.name}
66 </span>
67 <span style={{ fontSize: 12, color: ship.active ? '#4caf50' : '#f44336' }}>
68 {ship.active ? '● AKTYWNY' : '○ NIEAKTYWNY'}
69 </span>
70 </div>
71 <div style={{ margin: '8px 0' }}>
72 <div style={{ fontSize: 12, color: '#adb5bd', marginBottom: 4 }}>
73 Paliwo: {ship.fuel}%
74 </div>
75 <div style={{ background: '#0a0e1a', borderRadius: 4, height: 8 }}>
76 <div style={{
77 width: `${ship.fuel}%`, height: '100%', borderRadius: 4,
78 background: ship.fuel > 50 ? '#4caf50' : ship.fuel > 25 ? '#ff9800' : '#f44336',
79 }} />
80 </div>
81 </div>
82 <div style={{ display: 'flex', gap: 6 }}>
83 <button onClick={() => dispatch({ type: 'REFUEL', id: ship.id })} style={btnStyle}>
84 Zatankuj +20%
85 </button>
86 <button onClick={() => dispatch({ type: 'TOGGLE_ACTIVE', id: ship.id })} style={btnStyle}>
87 {ship.active ? 'Dezaktywuj' : 'Aktywuj'}
88 </button>
89 </div>
90 </div>
91 );
92});
93
94// Ten komponent re-renderuje się przy każdej zmianie stanu (czyta cały state)
95const FleetStats = memo(function FleetStats() {
96 const { ships } = useFleetState();
97 console.log('[RENDER] FleetStats');
98 const active = ships.filter(s => s.active).length;
99 const avgFuel = Math.round(ships.reduce((sum, s) => sum + s.fuel, 0) / ships.length);
100 return (
101 <div style={{ ...cardStyle, display: 'flex', gap: 20, justifyContent: 'center' }}>
102 <span style={{ color: '#adb5bd' }}>Aktywne: <b style={{ color: '#4fc3f7' }}>{active}/{ships.length}</b></span>
103 <span style={{ color: '#adb5bd' }}>Śr. paliwo: <b style={{ color: '#ffd700' }}>{avgFuel}%</b></span>
104 </div>
105 );
106});
107
108// Ten komponent tylko dispatchuje - NIE re-renderuje się przy zmianie stanu
109const ResetButton = memo(function ResetButton() {
110 const dispatch = useFleetDispatch();
111 console.log('[RENDER] ResetButton - powinien renderować się tylko raz!');
112 return (
113 <button
114 onClick={() => dispatch({ type: 'RESET' })}
115 style={{ ...btnStyle, width: '100%', padding: '10px', marginTop: 8 }}
116 >
117 Reset floty
118 </button>
119 );
120});
121
122function ShipList() {
123 const { ships } = useFleetState();
124 console.log('[RENDER] ShipList');
125 return (
126 <div>
127 {ships.map(ship => (
128 <ShipCard key={ship.id} ship={ship} />
129 ))}
130 </div>
131 );
132}
133
134export default function App() {
135 return (
136 <FleetProvider>
137 <div style={{ minHeight: '100vh', background: '#0a0e1a', padding: 24, fontFamily: 'Arial', color: 'white' }}>
138 <h1 style={{ textAlign: 'center', color: '#ffd700', marginBottom: 8 }}>
139 Optymalizacja Context API
140 </h1>
141 <p style={{ textAlign: 'center', color: '#778da9', marginBottom: 20, fontSize: 13 }}>
142 Otwórz konsolę (F12) aby zobaczyć które komponenty się re-renderują
143 </p>
144 <FleetStats />
145 <ShipList />
146 <ResetButton />
147 </div>
148 </FleetProvider>
149 );
150}
151
152const cardStyle = {
153 background: '#1a2035', borderRadius: 10, padding: 14,
154 marginBottom: 12, border: '1px solid rgba(255,255,255,0.1)',
155};
156const btnStyle = {
157 background: 'rgba(79,195,247,0.1)', color: '#4fc3f7',
158 border: '1px solid rgba(79,195,247,0.3)', borderRadius: 6,
159 padding: '5px 10px', cursor: 'pointer', fontSize: 12,
160};Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jak React.memo może pomóc w optymalizacji komponentów używających Context API?
2. Jak zapobiec niepotrzebnym re-renderom komponentów korzystających z Context API?
To 2 z 8 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Utwórz system uniwersytetu kosmicznego
- Edytor kodu
Implementuj laboratorium genetyczne
- Edytor kodu
Zbuduj galaktyczną sieć społecznościową
- Układanie w pionie
Uporządkuj zagnieżdżenie Providerów w typowej aplikacji React - od najzewnętrzniejszego:
- Edytor kodu
Zbuduj system Context API z useReducer dla zarządzania flotą: ThemeContext, FleetContext, dispatch actions i custom hooks
- Układanie w poziomie
Ułóż poprawne wywołanie dispatch z obiektem akcji:
- Układanie w pionie
Uporządkuj kiedy przejść z props na Context API - od najprostszego przypadku:
- Klikanie w kolejności
Uporządkuj kod Provider'a kosmicznego:
- Układanie w pionie
Uporządkuj Provider'y w hierarchii aplikacji kosmicznej:
- Układanie w pionie
Uporządkuj lifecycle events przy inicjalizacji kontekstu kosmicznego:
- Klikanie w kolejności
Uporządkuj custom hook dla kontekstu:
- Układanie w poziomie
Ułóż składnię tworzenia kontekstu w React:
- Klikanie w kolejności
Uporządkuj action creators kosmiczne:
- Układanie w poziomie
Ułóż składnię tworzenia nowego kontekstu z wartością domyślną:
- Układanie w pionie
Uporządkuj etapy testowania komponentów z Context API:
- Układanie w poziomie
Ułóż składnię inicjalizacji useReducer z reducerem i stanem początkowym:
- Układanie w pionie
Uporządkuj proces migracji starych systemów kosmicznych do Context API: