Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Optymalizacja Context API

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ą.

Vai a CodeWorlds