Cours JavaScript et React · Module 5: useEffect et hooks personnalisés

Hooks personnalisés pour les effets

6 min de lecture
Dans cette leçon6

Dans notre voyage cosmique à travers React, écrire encore et encore les mêmes schémas useEffect dans différents composants, c'est comme construire à partir de zéro des systèmes de navigation identiques pour chaque vaisseau de la flotte. Les Custom Hooks (hooks personnalisés) te permettent d'extraire la logique d'effet répétitive dans une seule fonction réutilisable - comme créer un module de propulsion qui peut être installé dans n'importe quel vaisseau.

Que sont les Custom Hooks ?

Un Custom Hook est une fonction JavaScript ordinaire dont le nom commence par use et qui peut appeler d'autres hooks (useState, useEffect, useRef, etc.). Ce n'est pas une syntaxe React spéciale - la seule convention est le préfixe use.

1// Custom Hook - logique extraite
2function useWindowSize() {
3  const [size, setSize] = useState({
4    width: window.innerWidth,
5    height: window.innerHeight
6  });
7
8  useEffect(() => {
9    const handleResize = () => {
10      setSize({ width: window.innerWidth, height: window.innerHeight });
11    };
12
13    window.addEventListener('resize', handleResize);
14    return () => window.removeEventListener('resize', handleResize);
15  }, []);
16
17  return size;
18}
19
20// Utilisation dans un composant - propre et simple
21function SpaceshipCockpit() {
22  const { width, height } = useWindowSize();
23
24  return (
25    <p>
26      Taille de l'écran du cockpit : {width} x {height}
27    </p>
28  );
29}

Remarque que useWindowSize contient useState et useEffect avec nettoyage à l'intérieur - toute la logique est enfermée dans un seul hook, et le composant obtient un résultat prêt à l'emploi.

useFetch - récupération de données

L'un des hooks personnalisés les plus courants est un hook de récupération de données. Au lieu de répéter le schéma loading/error/data dans chaque composant, nous l'encapsulons dans useFetch :

1function useFetch(url) {
2  const [data, setData] = useState(null);
3  const [isLoading, setIsLoading] = useState(true);
4  const [error, setError] = useState(null);
5
6  useEffect(() => {
7    const controller = new AbortController();
8    setIsLoading(true);
9    setError(null);
10
11    fetch(url, { signal: controller.signal })
12      .then(response => {
13        if (!response.ok) throw new Error('Erreur réseau');
14        return response.json();
15      })
16      .then(data => {
17        setData(data);
18        setIsLoading(false);
19      })
20      .catch(err => {
21        if (err.name !== 'AbortError') {
22          setError(err.message);
23          setIsLoading(false);
24        }
25      });
26
27    // Nettoyage - annuler la requête au démontage ou au changement d'URL
28    return () => controller.abort();
29  }, [url]);
30
31  return { data, isLoading, error };
32}

Maintenant, n'importe quel composant qui a besoin de données d'API peut utiliser une seule ligne :

1function CrewList() {
2  const { data: crew, isLoading, error } = useFetch('/api/crew');
3
4  if (isLoading) return <p>Chargement de l'équipage...</p>;
5  if (error) return <p>Erreur : {error}</p>;
6
7  return (
8    <ul>
9      {crew.map(member => (
10        <li key={member.id}>{member.name}</li>
11      ))}
12    </ul>
13  );
14}
15
16function MissionLog() {
17  const { data: missions, isLoading } = useFetch('/api/missions');
18
19  if (isLoading) return <p>Chargement des missions...</p>;
20
21  return missions.map(m => <div key={m.id}>{m.name}</div>);
22}

Deux composants, le même schéma de récupération de données - zéro duplication de code.

useInterval - minuterie sûre

Une minuterie avec setInterval nécessite un nettoyage et une gestion correcte des dépendances. Un hook personnalisé encapsule cette logique :

1function useInterval(callback, delay) {
2  const savedCallback = useRef();
3
4  // Mémoriser la dernière version du callback
5  useEffect(() => {
6    savedCallback.current = callback;
7  }, [callback]);
8
9  // Définir la minuterie
10  useEffect(() => {
11    if (delay === null) return; // null = mettre la minuterie en pause
12
13    const tick = () => savedCallback.current();
14    const id = setInterval(tick, delay);
15    return () => clearInterval(id);
16  }, [delay]);
17}
18
19// Utilisation
20function MissionClock() {
21  const [seconds, setSeconds] = useState(0);
22  const [isRunning, setIsRunning] = useState(false);
23
24  useInterval(() => {
25    setSeconds(prev => prev + 1);
26  }, isRunning ? 1000 : null); // null = pause
27
28  return (
29    <div>
30      <p>Temps de mission : {seconds}s</p>
31      <button onClick={() => setIsRunning(!isRunning)}>
32        {isRunning ? 'Pause' : 'Démarrer'}
33      </button>
34    </div>
35  );
36}

Grâce à useRef pour le callback, la minuterie appelle toujours la dernière version de la fonction sans avoir besoin de redémarrer l'intervalle.

useLocalStorage - état persistant

Un hook qui synchronise l'état avec localStorage :

1function useLocalStorage(key, initialValue) {
2  const [storedValue, setStoredValue] = useState(() => {
3    try {
4      const item = window.localStorage.getItem(key);
5      return item ? JSON.parse(item) : initialValue;
6    } catch (error) {
7      return initialValue;
8    }
9  });
10
11  useEffect(() => {
12    try {
13      window.localStorage.setItem(key, JSON.stringify(storedValue));
14    } catch (error) {
15      console.error('Erreur d\'écriture dans localStorage :', error);
16    }
17  }, [key, storedValue]);
18
19  return [storedValue, setStoredValue];
20}
21
22// Utilisation - fonctionne comme useState, mais persiste les données
23function ShipSettings() {
24  const [theme, setTheme] = useLocalStorage('theme', 'dark');
25  const [volume, setVolume] = useLocalStorage('volume', 50);
26
27  return (
28    <div>
29      <select value={theme} onChange={e => setTheme(e.target.value)}>
30        <option value="dark">Sombre</option>
31        <option value="light">Clair</option>
32      </select>
33      <input
34        type="range"
35        value={volume}
36        onChange={e => setVolume(Number(e.target.value))}
37      />
38    </div>
39  );
40}

Après avoir rafraîchi la page, les réglages sont conservés - localStorage mémorise les valeurs entre les sessions.

Règles pour créer des Custom Hooks

  1. Le nom commence par use - ce n'est pas optionnel. React exige ce préfixe pour faire respecter les règles des hooks
  2. Peut appeler d'autres hooks - useState, useEffect, useRef, et même d'autres hooks personnalisés
  3. Chaque appel a son propre état - deux composants utilisant le même hook ont des copies indépendantes de l'état
  4. Retourne ce dont le composant a besoin - peut retourner des valeurs, des tableaux, des objets, des fonctions
1// Custom Hook avec plusieurs valeurs de retour
2function useToggle(initialValue = false) {
3  const [value, setValue] = useState(initialValue);
4
5  const toggle = () => setValue(prev => !prev);
6  const setTrue = () => setValue(true);
7  const setFalse = () => setValue(false);
8
9  return { value, toggle, setTrue, setFalse };
10}
11
12// Utilisation
13function ShieldControl() {
14  const shields = useToggle(false);
15
16  return (
17    <div>
18      <p>Boucliers : {shields.value ? 'ACTIFS' : 'DÉSACTIVÉS'}</p>
19      <button onClick={shields.toggle}>Basculer</button>
20      <button onClick={shields.setTrue}>Activer</button>
21      <button onClick={shields.setFalse}>Désactiver</button>
22    </div>
23  );
24}

Résumé

Les Custom Hooks sont l'un des outils les plus puissants de React pour organiser le code :

  1. Éliminent la duplication - la logique d'effet répétitive en un seul endroit
  2. Améliorent la lisibilité - les composants deviennent plus courts et plus déclaratifs
  3. Facilitent les tests - la logique du hook peut être testée indépendamment du composant
  4. Sont composables - les hooks personnalisés peuvent appeler d'autres hooks personnalisés

Comme les systèmes modulaires d'un vaisseau spatial - système de navigation, système de communication, système de défense - chacun est un module distinct (hook) qui peut être installé dans n'importe quel vaisseau (composant).

Code de cette leçon : App.jsx
1import React, { useState, useEffect, useRef } from 'react';
2
3// Custom Hook: useToggle
4function useToggle(initialValue = false) {
5  const [value, setValue] = useState(initialValue);
6  const toggle = () => setValue(prev => !prev);
7  const setTrue = () => setValue(true);
8  const setFalse = () => setValue(false);
9  return { value, toggle, setTrue, setFalse };
10}
11
12// Custom Hook: useInterval
13function useInterval(callback, delay) {
14  const savedCallback = useRef();
15
16  useEffect(() => {
17    savedCallback.current = callback;
18  }, [callback]);
19
20  useEffect(() => {
21    if (delay === null) return;
22    const tick = () => savedCallback.current();
23    const id = setInterval(tick, delay);
24    return () => clearInterval(id);
25  }, [delay]);
26}
27
28// Custom Hook: useLocalStorage
29function useLocalStorage(key, initialValue) {
30  const [storedValue, setStoredValue] = useState(() => {
31    try {
32      const item = window.localStorage.getItem(key);
33      return item ? JSON.parse(item) : initialValue;
34    } catch {
35      return initialValue;
36    }
37  });
38
39  useEffect(() => {
40    try {
41      window.localStorage.setItem(key, JSON.stringify(storedValue));
42    } catch (error) {
43      console.error("Erreur d'écriture :", error);
44    }
45  }, [key, storedValue]);
46
47  return [storedValue, setStoredValue];
48}
49
50// Demo 1: useToggle
51function ShieldControls() {
52  const shields = useToggle(false);
53  const weapons = useToggle(false);
54  const engines = useToggle(true);
55
56  return (
57    <div className="demo-card">
58      <h2>useToggle</h2>
59      <div className="system-row">
60        <span>Boucliers</span>
61        <span className={shields.value ? 'on' : 'off'}>{shields.value ? 'ON' : 'OFF'}</span>
62        <button onClick={shields.toggle}>Basculer</button>
63      </div>
64      <div className="system-row">
65        <span>Armement</span>
66        <span className={weapons.value ? 'on' : 'off'}>{weapons.value ? 'ON' : 'OFF'}</span>
67        <button onClick={weapons.toggle}>Basculer</button>
68      </div>
69      <div className="system-row">
70        <span>Moteurs</span>
71        <span className={engines.value ? 'on' : 'off'}>{engines.value ? 'ON' : 'OFF'}</span>
72        <button onClick={engines.toggle}>Basculer</button>
73      </div>
74    </div>
75  );
76}
77
78// Demo 2: useInterval
79function MissionClock() {
80  const [seconds, setSeconds] = useState(0);
81  const isRunning = useToggle(false);
82
83  useInterval(() => {
84    setSeconds(prev => prev + 1);
85  }, isRunning.value ? 1000 : null);
86
87  const mins = String(Math.floor(seconds / 60)).padStart(2, '0');
88  const secs = String(seconds % 60).padStart(2, '0');
89
90  return (
91    <div className="demo-card">
92      <h2>useInterval</h2>
93      <div className="clock">{mins}:{secs}</div>
94      <div className="clock-controls">
95        <button onClick={isRunning.toggle}>
96          {isRunning.value ? 'Pause' : 'Start'}
97        </button>
98        <button onClick={() => { isRunning.setFalse(); setSeconds(0); }}>
99          Reset
100        </button>
101      </div>
102    </div>
103  );
104}
105
106// Demo 3: useLocalStorage
107function ShipSettings() {
108  const [pilotName, setPilotName] = useLocalStorage('pilotName', '');
109  const [theme, setTheme] = useLocalStorage('theme', 'dark');
110  const [volume, setVolume] = useLocalStorage('volume', 50);
111
112  return (
113    <div className="demo-card">
114      <h2>useLocalStorage</h2>
115      <p className="hint">Les données survivent au rafraîchissement de la page !</p>
116      <div className="setting">
117        <label>Pilote :</label>
118        <input
119          value={pilotName}
120          onChange={e => setPilotName(e.target.value)}
121          placeholder="Saisis un prénom..."
122        />
123      </div>
124      <div className="setting">
125        <label>Thème :</label>
126        <select value={theme} onChange={e => setTheme(e.target.value)}>
127          <option value="dark">Sombre</option>
128          <option value="light">Clair</option>
129          <option value="blue">Cosmique</option>
130        </select>
131      </div>
132      <div className="setting">
133        <label>Volume : {volume}%</label>
134        <input
135          type="range"
136          min="0"
137          max="100"
138          value={volume}
139          onChange={e => setVolume(Number(e.target.value))}
140        />
141      </div>
142    </div>
143  );
144}
145
146function App() {
147  return (
148    <div className="app">
149      <h1>Custom Hooks - Systèmes modulaires du vaisseau</h1>
150      <div className="demos">
151        <ShieldControls />
152        <MissionClock />
153        <ShipSettings />
154      </div>
155    </div>
156  );
157}
158
159export 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. 1. Lequel des éléments suivants n'est PAS un cas d'usage typique de useEffect ?

  2. 2. Quel hook React remplace la méthode componentDidMount des composants de classe ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Hook personnalisé de Media Query

  • Éditeur de code

    Surveillance de l'état de la batterie

Articles utiles