Cours JavaScript et React · Module 4: Context API et useReducer
Optimiser le Context API
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'interfaceUtilise 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. Comment React.memo peut-il aider à optimiser les composants utilisant le Context API ?
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 :