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.
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.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}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}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}Nie każde dane powinny trafić do kontekstu. Oto praktyczny przewodnik decyzyjny:
Używaj Context gdy:
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:
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}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ą.