Cours JavaScript et React · Module 4: Context API et useReducer
Patterns de gestion d'état
Dans cette leçon7
À la Station Jupiter, tu gères de nombreux systèmes simultanément - navigation, carburant, équipage, communications. Chaque système a besoin de données et doit les partager avec d'autres modules. Dans React, la gestion de l'état est l'un des défis architecturaux les plus importants. Choisir le bon pattern détermine si ton application sera facile à maintenir et évolutive, ou si elle deviendra un labyrinthe chaotique de dépendances.
Le problème : le prop drilling
Avant que tu n'apprennes le Context API, la seule façon de transmettre des données dans l'arbre des composants était le prop drilling - passer manuellement des props à travers chaque niveau. C'est comme envoyer des messages à travers une chaîne de membres d'équipage, où chacun doit répéter le message, même s'il n'en a pas besoin lui-même.
1// Prop drilling - 4 niveaux de transmission !
2function SpaceStation({ missionData }) {
3 return <Deck missionData={missionData} />;
4}
5
6function Deck({ missionData }) {
7 // Deck n'utilise pas missionData - il le transmet simplement
8 return <Room missionData={missionData} />;
9}
10
11function Room({ missionData }) {
12 // Room ne l'utilise pas non plus - il le transmet
13 return <Terminal missionData={missionData} />;
14}
15
16function Terminal({ missionData }) {
17 // Ce n'est qu'ici que les données sont réellement nécessaires !
18 return <p>Mission : {missionData.name}</p>;
19}À 4 niveaux, c'est encore gérable, mais à 8-10 niveaux (ce qui est normal dans les grandes applications), le code devient illisible, et chaque changement dans la structure des données nécessite de modifier de nombreux composants intermédiaires.
State colocation - garder l'état près de l'endroit où il est utilisé
Avant de recourir à des solutions globales, envisage la state colocation - le principe qui consiste à garder l'état aussi proche que possible des composants qui en ont besoin. Toutes les données n'ont pas besoin d'être globales. C'est comme sur un vaisseau spatial : la température du cockpit n'a pas besoin d'être connue dans la salle des machines.
1// MAUVAIS : État global pour des données locales
2function App() {
3 const [searchQuery, setSearchQuery] = useState('');
4 const [isModalOpen, setIsModalOpen] = useState(false);
5 const [formData, setFormData] = useState({});
6 // ... 20 autres états
7
8 return (
9 <GlobalContext.Provider value={/* tout */}>
10 <Dashboard />
11 </GlobalContext.Provider>
12 );
13}
14
15// BON : État proche de l'endroit où il est utilisé
16function App() {
17 // Uniquement les données vraiment globales
18 const [user, setUser] = useState(null);
19 const [theme, setTheme] = useState('dark');
20
21 return (
22 <AppContext.Provider value={{ user, theme }}>
23 <Dashboard />
24 </AppContext.Provider>
25 );
26}
27
28function SearchPanel() {
29 // État local - seul ce composant en a besoin
30 const [searchQuery, setSearchQuery] = useState('');
31 return <input value={searchQuery} onChange={e => setSearchQuery(e.target.value)} />;
32}Règle empirique : si l'état n'est utilisé que par un seul composant ou ses enfants directs - garde-le local. Remonte-le (lifting state up) ou place-le dans le contexte uniquement lorsque des branches éloignées de l'arbre des composants en ont besoin.
Context API - la solution intégrée
Le Context API résout le prop drilling sans bibliothèques supplémentaires. Tu l'as découvert plus tôt dans ce module. Ses principaux atouts et limites :
Atouts :
- Intégré à React - zéro dépendance supplémentaire
- API simple :
createContext,Provider,useContext - Excellent pour les données qui changent rarement (thème, auth, locale)
Limites :
- Chaque changement de valeur re-rend TOUS les consommateurs
- Pas de mise en cache intégrée pour les données d'API
- Nécessite une combinaison avec useReducer pour un état complexe
- Optimisation des performances difficile dans les grandes applications
1// Context API - bon choix pour la configuration du vaisseau
2const ShipConfigContext = createContext();
3
4function ShipConfigProvider({ children }) {
5 const [config, setConfig] = useState({
6 theme: 'dark-nebula',
7 language: 'en',
8 soundEnabled: true,
9 });
10
11 return (
12 <ShipConfigContext.Provider value={{ config, setConfig }}>
13 {children}
14 </ShipConfigContext.Provider>
15 );
16}Redux - centre de commandement pour les grandes flottes
Redux est un pattern de gestion d'état éprouvé, inspiré de l'architecture Flux. Il fonctionne comme le centre de commandement central d'une flotte - un seul store contient tout l'état de l'application, et les changements se produisent exclusivement via le dispatch d'actions.
1// Redux Toolkit - la manière moderne d'utiliser Redux
2// import { createSlice, configureStore } from '@reduxjs/toolkit';
3
4// Slice = un morceau d'état + reducers au même endroit
5const missionSlice = {
6 name: 'mission',
7 initialState: {
8 crew: [],
9 fuel: 100,
10 destination: null,
11 status: 'docked',
12 },
13 reducers: {
14 addCrewMember: (state, action) => {
15 state.crew.push(action.payload);
16 },
17 setDestination: (state, action) => {
18 state.destination = action.payload;
19 },
20 burnFuel: (state, action) => {
21 state.fuel = Math.max(0, state.fuel - action.payload);
22 },
23 launch: (state) => {
24 state.status = 'in-flight';
25 },
26 },
27};
28
29// Utilisation dans un composant :
30function MissionControl() {
31 // const dispatch = useDispatch();
32 // const { crew, fuel, status } = useSelector(state => state.mission);
33
34 // dispatch(missionSlice.actions.launch());
35 // dispatch(missionSlice.actions.burnFuel(10));
36}Quand utiliser Redux ?
- Applications avec de nombreux développeurs (règles claires, flux de données prévisible)
- État complexe avec de nombreuses actions liées
- Tu as besoin des DevTools avec le débogage time-travel
- Middleware pour gérer les effets de bord (thunks, sagas)
Quand NE PAS utiliser Redux ?
- Applications simples avec peu d'états
- Petite équipe ou développeur en solo
- Principalement des données d'API (React Query est meilleur)
Zustand - le propulseur de manœuvre léger
Zustand est une bibliothèque minimaliste de gestion d'état. Si Redux est le moteur principal, Zustand est le propulseur de manœuvre agile - il fait la même chose, mais avec moins de surcharge. Pas de providers, pas de code répétitif, pas de problèmes de re-rendus du Context API.
1// Zustand - store simple sans providers
2// import { create } from 'zustand';
3
4// Création d'un store - c'est tout ce dont tu as besoin !
5// const useShipStore = create((set) => ({
6// fuel: 100,
7// position: { x: 0, y: 0 },
8// crew: 5,
9//
10// burnFuel: (amount) => set((state) => ({
11// fuel: Math.max(0, state.fuel - amount)
12// })),
13//
14// moveTo: (x, y) => set({ position: { x, y } }),
15//
16// addCrew: () => set((state) => ({
17// crew: state.crew + 1
18// })),
19// }));
20
21// Utilisation dans un composant - pas besoin de Provider !
22function FuelGauge() {
23 // Le composant se re-rend UNIQUEMENT quand le carburant change
24 // const fuel = useShipStore((state) => state.fuel);
25 // return <div>Carburant : {fuel}%</div>;
26}
27
28function NavigationPanel() {
29 // Lit UNIQUEMENT la position - un changement de carburant ne provoque pas de re-rendu
30 // const position = useShipStore((state) => state.position);
31 // const moveTo = useShipStore((state) => state.moveTo);
32 // return <button onClick={() => moveTo(10, 20)}>Décoller !</button>;
33}Zustand optimise automatiquement les re-rendus grâce aux selectors. Chaque composant ne s'abonne qu'aux données qu'il utilise réellement. Un changement de carburant ne re-rend pas le panneau de navigation - et vice versa.
Comparaison des patterns - carte de la galaxie
| Caractéristique | Context API | Redux Toolkit | Zustand |
|---|---|---|---|
| Taille | 0 KB (intégré) | ~11 KB | ~1.5 KB |
| Code répétitif | Moyen | Important (mais RTK aide) | Minimal |
| Re-rendus | Tous les consommateurs | Optimisés (selectors) | Optimisés (selectors) |
| DevTools | Aucun dédié | Étendus (time-travel) | Extension Redux DevTools |
| Courbe d'apprentissage | Facile | Raide | Facile |
| Idéal pour | Thème, auth, locale | Grandes apps, grandes équipes | La plupart des applications |
Le principe de sélection de l'outil
À la Station Jupiter, chaque système a sa raison d'être. Il en va de même pour la gestion d'état :
- État local (
useState) - données nécessaires uniquement à un seul composant ou ses enfants directs - État remonté - données partagées par 2-3 composants proches, déplace-les dans leur parent commun
- Context API - données globales qui changent rarement : thème, auth, locale, langue
- Zustand - état global dans les applications petites/moyennes quand tu as besoin de performance et de simplicité
- Redux Toolkit - grandes applications avec de nombreux développeurs, logique métier complexe, besoin de DevTools
N'oublie pas : il n'existe pas de solution unique et idéale. Les meilleurs ingénieurs spatiaux adaptent l'outil au problème, et non le problème à l'outil.
Teste la comparaison des patterns dans l'éditeur ci-dessous :
Code de cette leçon : App.jsx
1import React, { createContext, useContext, useState, useReducer } from 'react';
2
3// === PATTERNS DE GESTION D'ÉTAT ===
4
5// Démo : Comparaison des patterns de gestion d'état
6// 1. Context API - intégré à React
7const ThemeContext = React.createContext();
8
9function ThemeProvider({ children }) {
10 const [theme, setTheme] = useState('dark-nebula');
11 const toggleTheme = () => setTheme(t => t === 'dark-nebula' ? 'light-star' : 'dark-nebula');
12 return (
13 <ThemeContext.Provider value={{ theme, toggleTheme }}>
14 {children}
15 </ThemeContext.Provider>
16 );
17}
18
19function useTheme() {
20 const ctx = useContext(ThemeContext);
21 if (!ctx) throw new Error('useTheme must be inside ThemeProvider');
22 return ctx;
23}
24
25// 2. useReducer - pattern de type Redux intégré à React
26const missionReducer = (state, action) => {
27 switch (action.type) {
28 case 'ADD_CREW':
29 return { ...state, crew: [...state.crew, action.payload] };
30 case 'SET_DESTINATION':
31 return { ...state, destination: action.payload };
32 case 'BURN_FUEL':
33 return { ...state, fuel: Math.max(0, state.fuel - action.payload) };
34 case 'LAUNCH':
35 return { ...state, status: 'in-flight' };
36 case 'DOCK':
37 return { ...state, status: 'docked', fuel: 100 };
38 default:
39 return state;
40 }
41};
42
43const initialMission = {
44 crew: ['Capitaine Ra', 'Navigatrice Nova'],
45 fuel: 100,
46 destination: null,
47 status: 'docked',
48};
49
50// === PANNEAUX DE DÉMONSTRATION ===
51
52function ContextDemo() {
53 const { theme, toggleTheme } = useTheme();
54 const isDark = theme === 'dark-nebula';
55 return (
56 <div style={{ ...styles.panel, background: isDark ? '#1a2035' : '#e8eaf6' }}>
57 <h3 style={{ color: '#4fc3f7' }}>Context API</h3>
58 <p style={{ color: isDark ? '#ccc' : '#333', fontSize: 13 }}>
59 Thème : <strong>{theme}</strong>
60 </p>
61 <p style={{ color: isDark ? '#888' : '#666', fontSize: 11 }}>
62 Idéal pour : thème, auth, locale
63 </p>
64 <button style={styles.btn} onClick={toggleTheme}>Changer de thème</button>
65 </div>
66 );
67}
68
69function ReducerDemo() {
70 const [state, dispatch] = useReducer(missionReducer, initialMission);
71 const destinations = ['Mars', 'Europa', 'Titan', 'Kepler-442b'];
72
73 return (
74 <div style={styles.panel}>
75 <h3 style={{ color: '#ffd700' }}>useReducer (type Redux)</h3>
76 <div style={styles.stats}>
77 <div>Statut : <span style={{ color: state.status === 'in-flight' ? '#69f0ae' : '#ffd700' }}>{state.status}</span></div>
78 <div>Carburant : <span style={{ color: state.fuel < 30 ? '#ff5252' : '#69f0ae' }}>{state.fuel}%</span></div>
79 <div>Destination : <span style={{ color: '#4fc3f7' }}>{state.destination || 'aucune'}</span></div>
80 <div>Équipage : {state.crew.join(', ')}</div>
81 </div>
82 <div style={styles.btnRow}>
83 {state.status === 'docked' ? (
84 <>
85 <button style={styles.btn} onClick={() => {
86 const dest = destinations[Math.floor(Math.random() * destinations.length)];
87 dispatch({ type: 'SET_DESTINATION', payload: dest });
88 }}>Choisir une destination</button>
89 {state.destination && (
90 <button style={{ ...styles.btn, background: 'rgba(105,240,174,0.2)', borderColor: '#69f0ae' }}
91 onClick={() => dispatch({ type: 'LAUNCH' })}>
92 Décoller !
93 </button>
94 )}
95 </>
96 ) : (
97 <>
98 <button style={styles.btn} onClick={() => dispatch({ type: 'BURN_FUEL', payload: 15 })}>
99 Moteurs (-15)
100 </button>
101 <button style={{ ...styles.btn, background: 'rgba(255,215,0,0.2)' }}
102 onClick={() => dispatch({ type: 'DOCK' })}>
103 Amarrer
104 </button>
105 </>
106 )}
107 </div>
108 </div>
109 );
110}
111
112function ColocationDemo() {
113 // État local - n'a pas besoin de Context ni de Redux
114 const [localSearch, setLocalSearch] = useState('');
115 const [results, setResults] = useState([]);
116
117 const planets = ['Mars', 'Jupiter', 'Saturn', 'Neptune', 'Venus', 'Mercury', 'Uranus', 'Pluto'];
118
119 const handleSearch = (query) => {
120 setLocalSearch(query);
121 if (query.length > 0) {
122 setResults(planets.filter(p => p.toLowerCase().includes(query.toLowerCase())));
123 } else {
124 setResults([]);
125 }
126 };
127
128 return (
129 <div style={styles.panel}>
130 <h3 style={{ color: '#ce93d8' }}>State Colocation</h3>
131 <p style={{ color: '#888', fontSize: 11, marginBottom: 8 }}>
132 État local - sans Context/Redux
133 </p>
134 <input
135 style={styles.input}
136 placeholder="Rechercher une planète..."
137 value={localSearch}
138 onChange={e => handleSearch(e.target.value)}
139 />
140 {results.length > 0 && (
141 <div style={styles.resultList}>
142 {results.map(r => (
143 <div key={r} style={styles.resultItem}>{r}</div>
144 ))}
145 </div>
146 )}
147 </div>
148 );
149}
150
151function ComparisonTable() {
152 const data = [
153 { pattern: 'useState', size: '0 KB', reRenders: 'Local', bestFor: 'État d\'un composant' },
154 { pattern: 'Context API', size: '0 KB', reRenders: 'Tous les consommateurs', bestFor: 'Thème, auth, locale' },
155 { pattern: 'useReducer', size: '0 KB', reRenders: 'Local', bestFor: 'Logique d\'état complexe' },
156 { pattern: 'Redux (RTK)', size: '~11 KB', reRenders: 'Sélectif', bestFor: 'Grandes apps, équipes' },
157 { pattern: 'Zustand', size: '~1.5 KB', reRenders: 'Sélectif', bestFor: 'La plupart des applications' },
158 ];
159
160 return (
161 <div style={{ ...styles.panel, gridColumn: '1 / -1' }}>
162 <h3 style={{ color: '#ffd700', textAlign: 'center' }}>Comparaison des patterns</h3>
163 <div style={styles.table}>
164 <div style={styles.tableHeader}>
165 <span>Pattern</span><span>Taille</span><span>Re-rendus</span><span>Idéal pour</span>
166 </div>
167 {data.map(row => (
168 <div key={row.pattern} style={styles.tableRow}>
169 <span style={{ color: '#4fc3f7' }}>{row.pattern}</span>
170 <span>{row.size}</span>
171 <span>{row.reRenders}</span>
172 <span style={{ color: '#888' }}>{row.bestFor}</span>
173 </div>
174 ))}
175 </div>
176 </div>
177 );
178}
179
180function App() {
181 return (
182 <ThemeProvider>
183 <div style={styles.container}>
184 <h1 style={styles.title}>Patterns de gestion d'état</h1>
185 <p style={styles.subtitle}>
186 Comparaison de Context API, useReducer et de la state colocation
187 </p>
188 <div style={styles.grid}>
189 <ContextDemo />
190 <ReducerDemo />
191 <ColocationDemo />
192 <ComparisonTable />
193 </div>
194 </div>
195 </ThemeProvider>
196 );
197}
198
199const styles = {
200 container: {
201 fontFamily: 'Arial, sans-serif',
202 background: 'linear-gradient(135deg, #0a0e1a 0%, #1a1a3e 100%)',
203 minHeight: '100vh',
204 padding: 24,
205 color: 'white',
206 },
207 title: { textAlign: 'center', color: '#ffd700', marginBottom: 4 },
208 subtitle: { textAlign: 'center', color: '#888', fontSize: 13, marginBottom: 24 },
209 grid: {
210 display: 'grid',
211 gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
212 gap: 16,
213 maxWidth: 900,
214 margin: '0 auto',
215 },
216 panel: {
217 background: '#1a2035',
218 borderRadius: 10,
219 padding: 16,
220 border: '1px solid rgba(79,195,247,0.2)',
221 },
222 stats: {
223 fontSize: 13,
224 lineHeight: 1.8,
225 marginBottom: 12,
226 padding: '8px 12px',
227 background: 'rgba(0,0,0,0.3)',
228 borderRadius: 8,
229 },
230 btnRow: { display: 'flex', gap: 8, justifyContent: 'center', flexWrap: 'wrap' },
231 btn: {
232 padding: '6px 16px',
233 background: 'rgba(255,255,255,0.08)',
234 color: 'white',
235 border: '1px solid rgba(255,255,255,0.15)',
236 borderRadius: 6,
237 cursor: 'pointer',
238 fontSize: 13,
239 },
240 input: {
241 width: '100%',
242 padding: '8px 12px',
243 background: 'rgba(0,0,0,0.3)',
244 color: 'white',
245 border: '1px solid rgba(79,195,247,0.3)',
246 borderRadius: 6,
247 fontSize: 13,
248 outline: 'none',
249 boxSizing: 'border-box',
250 },
251 resultList: {
252 marginTop: 8,
253 background: 'rgba(0,0,0,0.2)',
254 borderRadius: 6,
255 overflow: 'hidden',
256 },
257 resultItem: {
258 padding: '6px 12px',
259 borderBottom: '1px solid rgba(255,255,255,0.05)',
260 fontSize: 13,
261 color: '#ce93d8',
262 },
263 table: { fontSize: 12 },
264 tableHeader: {
265 display: 'grid',
266 gridTemplateColumns: '1fr 0.7fr 1fr 1.3fr',
267 gap: 8,
268 padding: '8px 0',
269 borderBottom: '1px solid rgba(255,215,0,0.3)',
270 fontWeight: 'bold',
271 color: '#ffd700',
272 },
273 tableRow: {
274 display: 'grid',
275 gridTemplateColumns: '1fr 0.7fr 1fr 1.3fr',
276 gap: 8,
277 padding: '6px 0',
278 borderBottom: '1px solid rgba(255,255,255,0.05)',
279 color: '#ccc',
280 },
281};
282
283export default App;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. Quand faut-il utiliser le Context API plutôt que de passer des props (prop drilling) ?
2. Que se passe-t-il lorsqu'un seul grand contexte stocke la position du vaisseau (mise à jour 60x/s) et le niveau de carburant, et qu'un composant FuelGauge ne lit que le carburant ?
Voici 2 des 5 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Crée trois contextes séparés (FuelContext, PositionContext, CrewContext) avec leurs propres providers. Compose-les ensemble dans ShipProviders. Affiche les données de chaque contexte dans des composants distincts.
- Clique dans l'ordre
Ordonne les étapes pour créer un custom hook d'accès sûr au contexte :
- Tri vertical
Ordonne les étapes de construction d'un système de contextes séparés, de l'extérieur vers l'intérieur :
- Éditeur de code
Implémente un custom hook useFuelContext avec validation de la disponibilité du contexte. Crée un FuelProvider et un composant FuelPanel qui utilise le hook.