Cours JavaScript et React · Module 4: Context API et useReducer
Gestion de l'état global
Dans cette leçon9
À mesure qu'une application React grandit, la gestion de l'état devient un défi de plus en plus complexe. Dans les petites applications, l'état local des composants et les props suffisent, mais à mesure que les choses prennent de l'ampleur, nous avons besoin de solutions plus avancées pour gérer l'état global de l'application.
Dans ce module, nous examinerons différentes approches de la gestion de l'état global dans React, des mécanismes intégrés aux bibliothèques externes populaires.
Les défis de l'état global
Avant de plonger dans les solutions, comprenons pourquoi la gestion de l'état global peut être difficile :
- Partage des données - des composants situés dans différentes parties de l'arbre des composants peuvent avoir besoin d'accéder aux mêmes données
- Synchronisation - les changements à un endroit doivent être visibles partout où les données sont utilisées
- Prévisibilité - les mises à jour de l'état doivent être prévisibles et faciles à tracer
- Performances - les re-rendus inutiles doivent être minimisés
- Complexité - la logique d'état peut être compliquée, avec de nombreux changements liés
Context API + useReducer comme solution de base
Comme nous l'avons appris dans les modules précédents, combiner le Context API avec useReducer offre une base solide pour la gestion de l'état global :
1import React, { createContext, useReducer, useContext } from 'react';
2
3// État initial
4const initialState = {
5 missions: [],
6 activeMission: null,
7 user: null,
8 isLoading: false,
9 error: null
10};
11
12// Reducer
13function appReducer(state, action) {
14 switch (action.type) {
15 case 'LOGIN_SUCCESS':
16 return { ...state, user: action.payload, error: null };
17 case 'LOGOUT':
18 return { ...state, user: null };
19 case 'FETCH_MISSIONS_START':
20 return { ...state, isLoading: true };
21 case 'FETCH_MISSIONS_SUCCESS':
22 return { ...state, missions: action.payload, isLoading: false };
23 case 'FETCH_MISSIONS_ERROR':
24 return { ...state, error: action.payload, isLoading: false };
25 case 'SET_ACTIVE_MISSION':
26 return { ...state, activeMission: action.payload };
27 default:
28 return state;
29 }
30}
31
32// Context
33const AppStateContext = createContext();
34
35// Provider
36function AppStateProvider({ children }) {
37 const [state, dispatch] = useReducer(appReducer, initialState);
38
39 // Nous pouvons aussi ajouter des fonctions utilitaires
40 const login = async (credentials) => {
41 try {
42 dispatch({ type: 'LOGIN_START' });
43 // Appel API pour la connexion
44 const user = await authService.login(credentials);
45 dispatch({ type: 'LOGIN_SUCCESS', payload: user });
46 } catch (error) {
47 dispatch({ type: 'LOGIN_ERROR', payload: error.message });
48 }
49 };
50
51 const fetchMissions = async () => {
52 try {
53 dispatch({ type: 'FETCH_MISSIONS_START' });
54 const missions = await missionService.getAll();
55 dispatch({ type: 'FETCH_MISSIONS_SUCCESS', payload: missions });
56 } catch (error) {
57 dispatch({ type: 'FETCH_MISSIONS_ERROR', payload: error.message });
58 }
59 };
60
61 // Valeur du contexte
62 const value = {
63 state,
64 dispatch,
65 login,
66 fetchMissions,
67 setActiveMission: (mission) => dispatch({ type: 'SET_ACTIVE_MISSION', payload: mission })
68 };
69
70 return (
71 <AppStateContext.Provider value={value}>
72 {children}
73 </AppStateContext.Provider>
74 );
75}
76
77// Hook pour utiliser le contexte
78function useAppState() {
79 const context = useContext(AppStateContext);
80 if (context === undefined) {
81 throw new Error('useAppState must be used within an AppStateProvider');
82 }
83 return context;
84}
85
86// Utilisation dans l'application
87function App() {
88 return (
89 <AppStateProvider>
90 <Header />
91 <Main />
92 <Footer />
93 </AppStateProvider>
94 );
95}
96
97function Header() {
98 const { state, dispatch } = useAppState();
99
100 return (
101 <header>
102 {state.user ? (
103 <>
104 <span>Bienvenue, {state.user.name}</span>
105 <button onClick={() => dispatch({ type: 'LOGOUT' })}>Déconnexion</button>
106 </>
107 ) : (
108 <LoginButton />
109 )}
110 </header>
111 );
112}
113
114function MissionsList() {
115 const { state, fetchMissions, setActiveMission } = useAppState();
116
117 useEffect(() => {
118 fetchMissions();
119 }, []);
120
121 if (state.isLoading) return <p>Chargement des missions...</p>;
122 if (state.error) return <p>Erreur : {state.error}</p>;
123
124 return (
125 <div>
126 <h2>Missions disponibles</h2>
127 <ul>
128 {state.missions.map(mission => (
129 <li key={mission.id}>
130 {mission.name}
131 <button onClick={() => setActiveMission(mission)}>
132 Sélectionner
133 </button>
134 </li>
135 ))}
136 </ul>
137 </div>
138 );
139}Cette solution présente plusieurs avantages :
- Elle utilise les mécanismes intégrés de React
- Elle ne nécessite aucune dépendance externe
- Elle offre de bonnes bases structurelles pour l'état global
Cependant, à mesure que l'application grandit, cette solution peut rencontrer des limites liées aux performances et à l'organisation du code.
Faire évoluer les applications avec plusieurs contextes
Pour les applications plus grandes, il est recommandé de découper l'état global en contextes plus petits et indépendants par domaine :
1// Contexte d'authentification
2const AuthContext = createContext();
3
4function AuthProvider({ children }) {
5 const [user, setUser] = useState(null);
6 const [isLoading, setIsLoading] = useState(false);
7 const [error, setError] = useState(null);
8
9 const login = async (credentials) => {
10 setIsLoading(true);
11 try {
12 const user = await authService.login(credentials);
13 setUser(user);
14 setError(null);
15 } catch (err) {
16 setError(err.message);
17 } finally {
18 setIsLoading(false);
19 }
20 };
21
22 const logout = async () => {
23 await authService.logout();
24 setUser(null);
25 };
26
27 return (
28 <AuthContext.Provider value={{ user, isLoading, error, login, logout }}>
29 {children}
30 </AuthContext.Provider>
31 );
32}
33
34// Contexte des missions
35const MissionsContext = createContext();
36
37function MissionsProvider({ children }) {
38 const [missions, setMissions] = useReducer(missionsReducer, []);
39 const [activeMission, setActiveMission] = useState(null);
40 // reste de la logique...
41
42 return (
43 <MissionsContext.Provider value={{ missions, activeMission, /*...*/ }}>
44 {children}
45 </MissionsContext.Provider>
46 );
47}
48
49// Contexte UI
50const UIContext = createContext();
51
52function UIProvider({ children }) {
53 const [darkMode, setDarkMode] = useState(false);
54 const [sidebarOpen, setSidebarOpen] = useState(true);
55 // reste de la logique...
56
57 return (
58 <UIContext.Provider value={{ darkMode, toggleDarkMode: () => setDarkMode(!darkMode), /*...*/ }}>
59 {children}
60 </UIContext.Provider>
61 );
62}
63
64// Combinaison de tous les providers
65function AppProviders({ children }) {
66 return (
67 <AuthProvider>
68 <MissionsProvider>
69 <UIProvider>
70 {children}
71 </UIProvider>
72 </MissionsProvider>
73 </AuthProvider>
74 );
75}Avantages de cette approche :
- Une meilleure séparation des responsabilités
- Des contextes plus petits = moins de re-rendus inutiles
- Des tests plus faciles des modules individuels
- Les composants ne peuvent utiliser que les contextes dont ils ont besoin
Optimisation des performances avec useMemo et memo
L'un des défis lors de l'utilisation du Context API est la performance - chaque changement de la valeur du contexte provoque un re-rendu de tous les composants qui l'utilisent. Voici quelques techniques d'optimisation :
1function OptimizedProvider({ children }) {
2 const [state, dispatch] = useReducer(reducer, initialState);
3
4 // Mémoïse la valeur du contexte
5 const contextValue = useMemo(() => {
6 return { state, dispatch };
7 }, [state, dispatch]);
8
9 return (
10 <AppContext.Provider value={contextValue}>
11 {children}
12 </AppContext.Provider>
13 );
14}
15
16// Optimisation des composants consommateurs
17const MissionItem = memo(function MissionItem({ mission, onSelect }) {
18 return (
19 <li>
20 {mission.name}
21 <button onClick={() => onSelect(mission)}>Sélectionner</button>
22 </li>
23 );
24});
25
26function MissionList() {
27 const { missions, selectMission } = useMissions();
28
29 // Mémoïse le callback
30 const handleSelect = useCallback((mission) => {
31 selectMission(mission);
32 }, [selectMission]);
33
34 return (
35 <ul>
36 {missions.map(mission => (
37 <MissionItem
38 key={mission.id}
39 mission={mission}
40 onSelect={handleSelect}
41 />
42 ))}
43 </ul>
44 );
45}Introduction à Redux
Redux est une bibliothèque populaire de gestion d'état qui offre une approche plus structurée de l'état global. Voici les bases de Redux avec React :
1import { createStore } from 'redux';
2import { Provider, useSelector, useDispatch } from 'react-redux';
3
4// Reducer
5const initialState = {
6 missions: [],
7 user: null
8};
9
10function spaceAppReducer(state = initialState, action) {
11 switch (action.type) {
12 case 'SET_USER':
13 return { ...state, user: action.payload };
14 case 'SET_MISSIONS':
15 return { ...state, missions: action.payload };
16 default:
17 return state;
18 }
19}
20
21// Store
22const store = createStore(spaceAppReducer);
23
24// Provider
25function App() {
26 return (
27 <Provider store={store}>
28 <Main />
29 </Provider>
30 );
31}
32
33// Utilisation dans les composants
34function UserProfile() {
35 // Sélection d'une partie de l'état
36 const user = useSelector(state => state.user);
37 const dispatch = useDispatch();
38
39 if (!user) return <p>Utilisateur non connecté</p>;
40
41 return (
42 <div>
43 <h2>{user.name}</h2>
44 <button onClick={() => dispatch({ type: 'SET_USER', payload: null })}>
45 Déconnexion
46 </button>
47 </div>
48 );
49}Organisation du code dans Redux
Dans les applications Redux plus grandes, le code est généralement organisé selon le pattern « ducks » ou par type :
1// /store/index.js
2import { createStore, combineReducers } from 'redux';
3import authReducer from './auth/reducer';
4import missionsReducer from './missions/reducer';
5import uiReducer from './ui/reducer';
6
7const rootReducer = combineReducers({
8 auth: authReducer,
9 missions: missionsReducer,
10 ui: uiReducer
11});
12
13const store = createStore(rootReducer);
14export default store;
15
16// /store/missions/actions.js
17export const fetchMissions = () => async (dispatch) => {
18 dispatch({ type: 'MISSIONS_LOADING' });
19 try {
20 const missions = await api.fetchMissions();
21 dispatch({ type: 'MISSIONS_SUCCESS', payload: missions });
22 } catch (error) {
23 dispatch({ type: 'MISSIONS_ERROR', payload: error.message });
24 }
25};
26
27// /store/missions/reducer.js
28const initialState = {
29 list: [],
30 active: null,
31 loading: false,
32 error: null
33};
34
35export default function missionsReducer(state = initialState, action) {
36 switch (action.type) {
37 case 'MISSIONS_LOADING':
38 return { ...state, loading: true };
39 case 'MISSIONS_SUCCESS':
40 return { ...state, list: action.payload, loading: false };
41 // autres cas...
42 default:
43 return state;
44 }
45}
46
47// /store/missions/selectors.js
48export const selectMissions = state => state.missions.list;
49export const selectActiveMission = state => state.missions.active;
50export const selectMissionsLoading = state => state.missions.loading;Alternatives modernes
De nombreuses bibliothèques alternatives de gestion d'état ont émergé dans l'écosystème React, visant à simplifier le processus et à réduire le code répétitif. Voici quelques options populaires :
1. Zustand
Zustand est une bibliothèque minimaliste qui combine la simplicité d'un hook avec la puissance d'un store :
1import create from 'zustand';
2
3// Création d'un store
4const useSpaceStore = create((set) => ({
5 missions: [],
6 activeMissionId: null,
7 isLoading: false,
8
9 fetchMissions: async () => {
10 set({ isLoading: true });
11 try {
12 const missions = await api.fetchMissions();
13 set({ missions, isLoading: false });
14 } catch (error) {
15 set({ error: error.message, isLoading: false });
16 }
17 },
18
19 setActiveMission: (id) => set({ activeMissionId: id })
20}));
21
22// Utilisation dans un composant
23function MissionPanel() {
24 // Récupération sélective des données depuis le store
25 const missions = useSpaceStore(state => state.missions);
26 const isLoading = useSpaceStore(state => state.isLoading);
27 const fetchMissions = useSpaceStore(state => state.fetchMissions);
28
29 useEffect(() => {
30 fetchMissions();
31 }, [fetchMissions]);
32
33 if (isLoading) return <p>Chargement...</p>;
34
35 return (
36 <div>
37 <h2>Missions</h2>
38 <ul>
39 {missions.map(mission => (
40 <MissionItem key={mission.id} mission={mission} />
41 ))}
42 </ul>
43 </div>
44 );
45}2. Recoil
Recoil est une bibliothèque créée par Facebook qui introduit les concepts d'« atomes » et de « selectors » :
1import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';
2
3// Définition des atomes (plus petites unités d'état)
4const missionsState = atom({
5 key: 'missionsState',
6 default: []
7});
8
9const missionsFilterState = atom({
10 key: 'missionsFilterState',
11 default: 'all' // 'all', 'active', 'completed'
12});
13
14// Selector (état dérivé)
15const filteredMissionsState = selector({
16 key: 'filteredMissionsState',
17 get: ({ get }) => {
18 const missions = get(missionsState);
19 const filter = get(missionsFilterState);
20
21 switch (filter) {
22 case 'active':
23 return missions.filter(mission => mission.status === 'active');
24 case 'completed':
25 return missions.filter(mission => mission.status === 'completed');
26 default:
27 return missions;
28 }
29 }
30});
31
32// Utilisation dans les composants
33function MissionsFilter() {
34 const [filter, setFilter] = useRecoilState(missionsFilterState);
35
36 return (
37 <div>
38 <button
39 className={filter === 'all' ? 'active' : ''}
40 onClick={() => setFilter('all')}
41 >
42 Toutes
43 </button>
44 <button
45 className={filter === 'active' ? 'active' : ''}
46 onClick={() => setFilter('active')}
47 >
48 Actives
49 </button>
50 <button
51 className={filter === 'completed' ? 'active' : ''}
52 onClick={() => setFilter('completed')}
53 >
54 Terminées
55 </button>
56 </div>
57 );
58}
59
60function MissionsList() {
61 // Utilisation de la valeur filtrée
62 const missions = useRecoilValue(filteredMissionsState);
63
64 return (
65 <ul>
66 {missions.map(mission => (
67 <li key={mission.id}>{mission.name}</li>
68 ))}
69 </ul>
70 );
71}3. Jotai
Jotai, similaire à Recoil, utilise une approche basée sur les atomes mais avec une API plus minimaliste :
1import { atom, useAtom } from 'jotai';
2
3// Définition des atomes
4const userAtom = atom(null);
5const missionsAtom = atom([]);
6const missionFilterAtom = atom('all');
7
8// Atome dérivé
9const filteredMissionsAtom = atom(
10 (get) => {
11 const missions = get(missionsAtom);
12 const filter = get(missionFilterAtom);
13
14 switch (filter) {
15 case 'active':
16 return missions.filter(m => m.status === 'active');
17 case 'completed':
18 return missions.filter(m => m.status === 'completed');
19 default:
20 return missions;
21 }
22 }
23);
24
25// Composant
26function MissionDashboard() {
27 const [missions, setMissions] = useAtom(missionsAtom);
28 const [filter, setFilter] = useAtom(missionFilterAtom);
29 const [filteredMissions] = useAtom(filteredMissionsAtom);
30
31 // Logique du composant...
32
33 return (
34 <div>
35 {/* UI */}
36 </div>
37 );
38}Choisir la bonne solution
Comment choisir la meilleure solution de gestion d'état pour ton application ? Voici quelques repères :
Taille de l'application :
- Petites applications : useState + useReducer + Context API
- Applications moyennes : Zustand ou Jotai
- Grandes applications : Redux (avec Redux Toolkit) ou Recoil
Complexité de l'état :
- État simple : useState
- État moyennement complexe : useReducer + Context API
- État complexe avec des relations : Redux ou Recoil
Exigences de performance :
- Si la performance de rendu est critique, envisage des bibliothèques qui offrent une réactivité granulaire (Recoil, Jotai)
Expérience de l'équipe :
- Si l'équipe connaît Redux, il peut valoir la peine de s'y tenir
- Pour de nouveaux projets, des API plus simples comme Zustand ou Jotai peuvent accélérer le développement
Bonnes pratiques
Quelle que soit la solution choisie, voici quelques bonnes pratiques générales pour la gestion de l'état global :
Minimise l'état global - tout n'a pas besoin d'être global ; utilise l'état local pour les données spécifiques à un composant
Découpe l'état par domaines logiques - regroupe les données et la logique liées
Normalise les données - évite la duplication et l'imbrication profonde
Maintiens l'immuabilité - ne modifie jamais l'état directement
Documente les intentions - utilise des noms descriptifs pour les actions et les selectors
Utilise des selectors - pour récupérer des données dérivées
Garde la logique métier hors des composants - dans les actions, les reducers ou des hooks dédiés
1// Exemple : Hook avec logique métier
2function useMissionControl() {
3 const { state, dispatch } = useAppState();
4
5 const launchMission = useCallback(async (missionId) => {
6 // Vérifie les conditions métier
7 if (!state.user || state.user.role !== 'commander') {
8 throw new Error('Seul le commandant peut démarrer une mission');
9 }
10
11 if (state.weather === 'storm') {
12 throw new Error('Impossible de lancer pendant une tempête');
13 }
14
15 // Logique métier
16 dispatch({ type: 'MISSION_LAUNCH_START', payload: missionId });
17
18 try {
19 await missionService.launch(missionId);
20 dispatch({ type: 'MISSION_LAUNCH_SUCCESS', payload: missionId });
21 } catch (error) {
22 dispatch({ type: 'MISSION_LAUNCH_FAILURE', payload: error.message });
23 throw error;
24 }
25 }, [state.user, state.weather, dispatch]);
26
27 return {
28 missions: state.missions,
29 activeMission: state.activeMission,
30 isLaunching: state.isLaunching,
31 launchError: state.launchError,
32 launchMission
33 };
34}Résumé
La gestion de l'état global est un aspect clé de la construction d'applications React évolutives. Dans ce module, nous avons découvert différentes approches :
Context API + useReducer - la solution intégrée de React, bonne pour les petites et moyennes applications
Découpage en plusieurs contextes - un moyen d'organiser l'état global par domaine
Optimisation des performances - des techniques pour minimiser les re-rendus
Redux - une bibliothèque traditionnelle avec une approche structurée de la gestion d'état
Alternatives modernes - Zustand, Recoil, Jotai offrant des API plus simples et moins de code
Le choix de la bonne solution dépend de la taille de l'application, de la complexité de l'état et des préférences de l'équipe. Quel que soit le choix, suivre les bonnes pratiques aidera à maintenir un code propre, prévisible et performant.
Code de cette leçon : App.jsx
1import React, { useState, useEffect, useCallback, useRef } from 'react';
2
3// Custom Hook : useLocalStorage
4function useLocalStorage(key, initialValue) {
5 const [storedValue, setStoredValue] = useState(() => {
6 try {
7 const item = window.localStorage.getItem(key);
8 return item ? JSON.parse(item) : initialValue;
9 } catch (error) {
10 return initialValue;
11 }
12 });
13
14 const setValue = (value) => {
15 try {
16 const valueToStore = value instanceof Function ? value(storedValue) : value;
17 setStoredValue(valueToStore);
18 window.localStorage.setItem(key, JSON.stringify(valueToStore));
19 } catch (error) {
20 console.log(error);
21 }
22 };
23
24 return [storedValue, setValue];
25}
26
27// Custom Hook : useToggle
28function useToggle(initialValue = false) {
29 const [value, setValue] = useState(initialValue);
30 const toggle = useCallback(() => setValue(v => !v), []);
31 return [value, toggle];
32}
33
34// Custom Hook : useCounter
35function useCounter(initialValue = 0, step = 1) {
36 const [count, setCount] = useState(initialValue);
37
38 const increment = useCallback(() => setCount(c => c + step), [step]);
39 const decrement = useCallback(() => setCount(c => c - step), [step]);
40 const reset = useCallback(() => setCount(initialValue), [initialValue]);
41
42 return { count, increment, decrement, reset };
43}
44
45// Custom Hook : useFetch
46function useFetch(url) {
47 const [data, setData] = useState(null);
48 const [loading, setLoading] = useState(true);
49 const [error, setError] = useState(null);
50
51 useEffect(() => {
52 const fetchData = async () => {
53 setLoading(true);
54 // Simulation de fetch
55 setTimeout(() => {
56 setData({
57 ships: ['Explorer-1', 'Titan-7', 'Guardian-3'],
58 missions: 12,
59 crew: 45
60 });
61 setLoading(false);
62 }, 1500);
63 };
64 fetchData();
65 }, [url]);
66
67 return { data, loading, error };
68}
69
70// Custom Hook : useWindowSize
71function useWindowSize() {
72 const [size, setSize] = useState({
73 width: window.innerWidth,
74 height: window.innerHeight
75 });
76
77 useEffect(() => {
78 const handleResize = () => {
79 setSize({ width: window.innerWidth, height: window.innerHeight });
80 };
81 window.addEventListener('resize', handleResize);
82 return () => window.removeEventListener('resize', handleResize);
83 }, []);
84
85 return size;
86}
87
88// Custom Hook : usePrevious
89function usePrevious(value) {
90 const ref = useRef();
91 useEffect(() => {
92 ref.current = value;
93 }, [value]);
94 return ref.current;
95}
96
97// Composant de démonstration
98function CustomHooksDemo() {
99 // démo useLocalStorage
100 const [name, setName] = useLocalStorage('astronaut-name', 'Cosmonaute');
101
102 // démo useToggle
103 const [isShieldActive, toggleShield] = useToggle(false);
104
105 // démo useCounter
106 const { count: fuel, increment: addFuel, decrement: useFuel, reset: resetFuel } = useCounter(50, 10);
107
108 // démo useFetch
109 const { data, loading, error } = useFetch('/api/station');
110
111 // démo useWindowSize
112 const { width, height } = useWindowSize();
113
114 // démo usePrevious
115 const previousFuel = usePrevious(fuel);
116
117 return (
118 <div style={{
119 fontFamily: 'Arial, sans-serif',
120 padding: '20px',
121 maxWidth: '800px',
122 margin: '0 auto',
123 backgroundColor: '#0a0a1a',
124 minHeight: '100vh',
125 color: 'white'
126 }}>
127 <h1 style={{ textAlign: 'center', color: '#00d4ff' }}>Custom Hooks</h1>
128
129 {/* useLocalStorage */}
130 <section style={{ marginBottom: '30px', padding: '20px', backgroundColor: '#1a1a2e', borderRadius: '12px' }}>
131 <h2 style={{ color: '#4caf50' }}>useLocalStorage</h2>
132 <p>Nom de l'astronaute (enregistré dans localStorage) :</p>
133 <input
134 type="text"
135 value={name}
136 onChange={(e) => setName(e.target.value)}
137 style={{
138 padding: '10px',
139 width: '100%',
140 boxSizing: 'border-box',
141 borderRadius: '8px',
142 border: '1px solid #333',
143 backgroundColor: '#252545',
144 color: 'white'
145 }}
146 />
147 <p style={{ color: '#888', fontSize: '14px' }}>Recharge la page - la valeur sera conservée !</p>
148 </section>
149
150 {/* useToggle */}
151 <section style={{ marginBottom: '30px', padding: '20px', backgroundColor: '#1a1a2e', borderRadius: '12px' }}>
152 <h2 style={{ color: '#ff9800' }}>useToggle</h2>
153 <div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
154 <button
155 onClick={toggleShield}
156 style={{
157 padding: '12px 24px',
158 backgroundColor: isShieldActive ? '#4caf50' : '#f44336',
159 color: 'white',
160 border: 'none',
161 borderRadius: '8px',
162 cursor: 'pointer'
163 }}
164 >
165 {isShieldActive ? 'Boucliers ACTIFS' : 'Boucliers HORS LIGNE'}
166 </button>
167 </div>
168 </section>
169
170 {/* useCounter */}
171 <section style={{ marginBottom: '30px', padding: '20px', backgroundColor: '#1a1a2e', borderRadius: '12px' }}>
172 <h2 style={{ color: '#2196f3' }}>useCounter</h2>
173 <div style={{ textAlign: 'center' }}>
174 <div style={{ fontSize: '48px', fontWeight: 'bold', color: fuel < 30 ? '#f44336' : '#4caf50' }}>
175 {fuel}%
176 </div>
177 <p>Carburant (précédemment : {previousFuel ?? '-'}%)</p>
178 <div style={{ display: 'flex', gap: '10px', justifyContent: 'center' }}>
179 <button onClick={useFuel} style={{ padding: '10px 20px', backgroundColor: '#f44336', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>-10</button>
180 <button onClick={resetFuel} style={{ padding: '10px 20px', backgroundColor: '#666', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>Reset</button>
181 <button onClick={addFuel} style={{ padding: '10px 20px', backgroundColor: '#4caf50', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>+10</button>
182 </div>
183 </div>
184 </section>
185
186 {/* useFetch */}
187 <section style={{ marginBottom: '30px', padding: '20px', backgroundColor: '#1a1a2e', borderRadius: '12px' }}>
188 <h2 style={{ color: '#9c27b0' }}>useFetch</h2>
189 {loading ? (
190 <p>Chargement des données de la station... ⏳</p>
191 ) : (
192 <div>
193 <p>Vaisseaux : {data?.ships?.join(', ')}</p>
194 <p>Missions actives : {data?.missions}</p>
195 <p>Membres d'équipage : {data?.crew}</p>
196 </div>
197 )}
198 </section>
199
200 {/* useWindowSize */}
201 <section style={{ padding: '20px', backgroundColor: '#1a1a2e', borderRadius: '12px' }}>
202 <h2 style={{ color: '#e91e63' }}>useWindowSize</h2>
203 <p>Taille de la fenêtre : {width} x {height} px</p>
204 <p style={{ color: '#888', fontSize: '14px' }}>Redimensionne la fenêtre du navigateur !</p>
205 </section>
206 </div>
207 );
208}
209
210export default CustomHooksDemo;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 la valeur par défaut passée à createContext est-elle utilisée ?
2. Que se passe-t-il lorsqu'un composant se trouve entre deux Providers du même contexte ?
Tâches pratiques dans le jeu
- Tri vertical
Ordonne ce qui se passe quand la valeur d'un Provider change :
- Éditeur de code
Implémente un système d'inventaire spatial
- Tri vertical
Ordonne les étapes pour créer un système de gestion de flotte galactique :
- Éditeur de code
Implémente un centre de commandement de flotte