Cours JavaScript et React · Module 4: Context API et useReducer

Optimiser le Context API

6 min de lecture
Dans cette leçon6

Un centre de commandement de mission traite des centaines de signaux par seconde. Si chaque signal provoquait un redémarrage de tous les systèmes - le vaisseau n'atteindrait jamais l'orbite. React fonctionne de manière similaire : lorsque la valeur d'un contexte change, chaque composant qui consomme ce contexte est re-rendu. Dans les petites applications, cela passe inaperçu, mais dans les systèmes complexes, cela peut considérablement ralentir les choses.

Le problème : des re-rendus excessifs avec le contexte

Considérons un exemple simple. Nous avons un seul contexte stockant tout l'état du vaisseau :

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 se rend');
21  return <p>Carburant : {state.fuel}%</p>;
22}
23
24function AlertBell() {
25  const { state } = useContext(ShipContext);
26  console.log('AlertBell se rend');
27  return <p>Alertes : {state.alerts.length}</p>;
28}

Lorsque alerts change (par ex. une nouvelle alerte), FuelGauge se re-rend aussi - même si le niveau de carburant n'a pas changé. Tout changement d'un champ quelconque de state déclenche le rendu de tous les consommateurs du contexte.

Solution 1 : Découper le contexte en state et dispatch

L'optimisation la plus simple et la plus efficace consiste à découper le contexte en deux contextes distincts : un pour l'état (qui change fréquemment), un autre pour dispatch (qui reste stable durant toute la vie du composant) :

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// Hooks dédiés pour chaque contexte
17function useShipState() {
18  return useContext(ShipStateContext);
19}
20
21function useShipDispatch() {
22  return useContext(ShipDispatchContext);
23}

Pourquoi cela fonctionne-t-il ? Le dispatch retourné par useReducer est une référence stable - React garantit qu'il ne change pas entre les rendus. Les composants qui ne font qu'envoyer des actions (et n'affichent pas l'état) ne seront pas re-rendus lorsque l'état change :

1// Ce composant se re-rend uniquement quand l'état change
2function FuelGauge() {
3  const state = useShipState();
4  return <p>Carburant : {state.fuel}%</p>;
5}
6
7// Ce composant ne se re-rend PAS quand l'état du vaisseau change
8// (car il n'utilise que dispatch, et dispatch est stable)
9function LaunchButton() {
10  const dispatch = useShipDispatch();
11  return (
12    <button onClick={() => dispatch({ type: 'LAUNCH' })}>
13      Lancer
14    </button>
15  );
16}

Solution 2 : Découper en plusieurs contextes spécialisés

Au lieu d'un seul contexte pour tout l'état, nous pouvons créer des contextes distincts pour différents domaines fonctionnels. Les composants s'abonnent uniquement au contexte qui les concerne :

1// Contexte distinct pour le carburant
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// Contexte distinct pour les alertes
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 se re-rend uniquement quand le carburant change
32function FuelGauge() {
33  const { fuel } = useContext(FuelContext);
34  return <p>Carburant : {fuel}%</p>;
35}
36
37// AlertBell se re-rend uniquement quand les alertes changent
38function AlertBell() {
39  const { alerts } = useContext(AlertContext);
40  return <p>Alertes : {alerts.length}</p>;
41}

Solution 3 : React.memo pour les composants enfants

React.memo est un HOC (Higher-Order Component) qui enveloppe un composant et garantit qu'il ne se re-rend que lorsque ses props ont changé. C'est particulièrement utile pour les composants enfants des providers :

1// Sans memo - se re-rend à chaque fois que le parent se re-rend
2function CrewMember({ name, role }) {
3  console.log(`Rendu : ${name}`);
4  return <li>{name} - {role}</li>;
5}
6
7// Avec memo - se re-rend uniquement quand name ou role changent
8const CrewMemberOptimized = React.memo(function CrewMember({ name, role }) {
9  console.log(`Rendu : ${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 ne se re-rend pas quand le carburant ou le statut change
19        <CrewMemberOptimized
20          key={member.id}
21          name={member.name}
22          role={member.role}
23        />
24      ))}
25    </ul>
26  );
27}

Quand le contexte, quand les props ?

Toutes les données ne doivent pas aller dans le contexte. Voici un guide de décision pratique :

Utilise le contexte quand :

  • Les données sont nécessaires dans de nombreux composants à différents niveaux de l'arbre
  • Tu évites de transmettre des props sur plus de 2-3 niveaux (prop drilling)
  • Les données changent rarement ou concernent le contexte large de l'application (thème, langue, données utilisateur)
1// Bons candidats pour le contexte
2const ThemeContext = createContext();        // thème (sombre/clair)
3const AuthContext = createContext();         // utilisateur connecté
4const LanguageContext = createContext();     // langue de l'interface

Utilise les props quand :

  • Les données ne transitent que par 1-2 niveaux de composants
  • Le composant est utilisé plusieurs fois dans différents contextes (réutilisable)
  • Les données sont spécifiques à une section particulière de l'interface
1// Mieux en props - ce sont des données spécifiques au composant
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}

Règle pratique

Avant d'ajouter quelque chose au contexte, demande-toi : « Ai-je vraiment besoin de cela globalement, ou n'est-ce que du prop drilling que je peux résoudre avec une meilleure structure de composants ? »

1// Parfois, le problème de prop drilling peut être résolu par la composition
2// plutôt que par le Context :
3
4// Au lieu de transmettre onSelectMission à travers 3 niveaux...
5function MissionControl() {
6  const [selected, setSelected] = useState(null);
7
8  return (
9    // Passe un composant prêt comme children, au lieu d'une fonction comme 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}

L'optimisation du contexte est un équilibre entre commodité et performance. Pour la plupart des applications, un découpage de base en un contexte d'état et un contexte de dispatch suffit. Les techniques plus avancées valent la peine d'être appliquées lorsque tu observes réellement des problèmes de performance.

Code de cette leçon : App.jsx
1import React, { createContext, useContext, useReducer, memo } from 'react';
2
3// --- Contextes distincts pour l'état et le dispatch ---
4// Optimisation : les composants qui ne font que dispatcher ne se re-rendent pas au changement d'état
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// Hooks d'accès au contexte
52function useFleetState() { return useContext(StateContext); }
53function useFleetDispatch() { return useContext(DispatchContext); }
54
55// --- Composants optimisés avec React.memo ---
56
57// Ce composant se re-rend UNIQUEMENT quand un vaisseau spécifique change
58const ShipCard = memo(function ShipCard({ ship }) {
59  const dispatch = useFleetDispatch();
60  console.log(`[RENDU] 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 ? '● ACTIF' : '○ INACTIF'}
69        </span>
70      </div>
71      <div style={{ margin: '8px 0' }}>
72        <div style={{ fontSize: 12, color: '#adb5bd', marginBottom: 4 }}>
73          Carburant : {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          Plein +20%
85        </button>
86        <button onClick={() => dispatch({ type: 'TOGGLE_ACTIVE', id: ship.id })} style={btnStyle}>
87          {ship.active ? 'Désactiver' : 'Activer'}
88        </button>
89      </div>
90    </div>
91  );
92});
93
94// Ce composant se re-rend à chaque changement d'état (lit tout l'état)
95const FleetStats = memo(function FleetStats() {
96  const { ships } = useFleetState();
97  console.log('[RENDU] 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' }}>Actifs : <b style={{ color: '#4fc3f7' }}>{active}/{ships.length}</b></span>
103      <span style={{ color: '#adb5bd' }}>Carburant moyen : <b style={{ color: '#ffd700' }}>{avgFuel}%</b></span>
104    </div>
105  );
106});
107
108// Ce composant ne fait que dispatcher - il ne se re-rend PAS au changement d'état
109const ResetButton = memo(function ResetButton() {
110  const dispatch = useFleetDispatch();
111  console.log('[RENDU] ResetButton - ne devrait se rendre qu\'une seule fois !');
112  return (
113    <button
114      onClick={() => dispatch({ type: 'RESET' })}
115      style={{ ...btnStyle, width: '100%', padding: '10px', marginTop: 8 }}
116    >
117      Réinitialiser la flotte
118    </button>
119  );
120});
121
122function ShipList() {
123  const { ships } = useFleetState();
124  console.log('[RENDU] 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          Optimisation du Context API
140        </h1>
141        <p style={{ textAlign: 'center', color: '#778da9', marginBottom: 20, fontSize: 13 }}>
142          Ouvre la console (F12) pour voir quels composants se re-rendent
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};

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Comment React.memo peut-il aider à optimiser les composants utilisant le Context API ?

  2. 2. Comment éviter les re-rendus inutiles dans les composants utilisant le Context API ?

Voici 2 des 8 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Crée un système d'université spatiale

  • Éditeur de code

    Implémente un laboratoire génétique

  • Éditeur de code

    Construis un réseau social galactique

  • Tri vertical

    Ordonne l'imbrication des Providers dans une application React typique - du plus externe :

  • Éditeur de code

    Construis un système Context API avec useReducer pour la gestion de flotte : ThemeContext, FleetContext, actions dispatch et custom hooks

  • Tri horizontal

    Ordonne un appel correct de dispatch avec un objet action :

  • Tri vertical

    Ordonne quand passer des props au Context API - du cas le plus simple :

  • Clique dans l'ordre

    Ordonne le code d'un Provider spatial :

  • Tri vertical

    Ordonne les Providers dans la hiérarchie d'une application spatiale :

  • Tri vertical

    Ordonne les événements du cycle de vie lors de l'initialisation d'un contexte spatial :

  • Clique dans l'ordre

    Ordonne le custom hook pour le contexte :

  • Tri horizontal

    Ordonne la syntaxe pour créer un contexte dans React :

  • Clique dans l'ordre

    Ordonne les action creators spatiaux :

  • Tri horizontal

    Ordonne la syntaxe pour créer un nouveau contexte avec une valeur par défaut :

  • Tri vertical

    Ordonne les étapes pour tester des composants avec le Context API :

  • Tri horizontal

    Ordonne la syntaxe pour initialiser useReducer avec un reducer et un état initial :

  • Tri vertical

    Ordonne le processus de migration d'anciens systèmes spatiaux vers le Context API :

Articles utiles