Kurs JavaScript i React · Moduł 4: Context API i useReducer

Optymalizacja Context API

5 min czytania
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 interfejsu

Uż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. 1. Jak React.memo może pomóc w optymalizacji komponentów używających Context API?

  2. 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:

Przydatne artykuły