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

Gestion de l'état global

12 min de lecture
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 :

  1. 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
  2. Synchronisation - les changements à un endroit doivent être visibles partout où les données sont utilisées
  3. Prévisibilité - les mises à jour de l'état doivent être prévisibles et faciles à tracer
  4. Performances - les re-rendus inutiles doivent être minimisés
  5. 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 :

  1. Taille de l'application :

    • Petites applications : useState + useReducer + Context API
    • Applications moyennes : Zustand ou Jotai
    • Grandes applications : Redux (avec Redux Toolkit) ou Recoil
  2. Complexité de l'état :

    • État simple : useState
    • État moyennement complexe : useReducer + Context API
    • État complexe avec des relations : Redux ou Recoil
  3. Exigences de performance :

    • Si la performance de rendu est critique, envisage des bibliothèques qui offrent une réactivité granulaire (Recoil, Jotai)
  4. 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 :

  1. Minimise l'état global - tout n'a pas besoin d'être global ; utilise l'état local pour les données spécifiques à un composant

  2. Découpe l'état par domaines logiques - regroupe les données et la logique liées

  3. Normalise les données - évite la duplication et l'imbrication profonde

  4. Maintiens l'immuabilité - ne modifie jamais l'état directement

  5. Documente les intentions - utilise des noms descriptifs pour les actions et les selectors

  6. Utilise des selectors - pour récupérer des données dérivées

  7. 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 :

  1. Context API + useReducer - la solution intégrée de React, bonne pour les petites et moyennes applications

  2. Découpage en plusieurs contextes - un moyen d'organiser l'état global par domaine

  3. Optimisation des performances - des techniques pour minimiser les re-rendus

  4. Redux - une bibliothèque traditionnelle avec une approche structurée de la gestion d'état

  5. 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. 1. Quand la valeur par défaut passée à createContext est-elle utilisée ?

  2. 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

Articles utiles