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

Créer des hooks personnalisés - modèles avancés

6 min de lecture
Dans cette leçon7

Dans la flotte cosmique de React, l'une des compétences d'ingénierie les plus précieuses est la conception de modules réutilisables. Imagine que tu construis des systèmes de navigation, de communication et de diagnostic pour des centaines de vaisseaux spatiaux. Au lieu d'écrire chaque système à partir de zéro pour chaque vaisseau, tu crées des modules universels qui peuvent être connectés à n'importe quel véhicule. C'est exactement ainsi que fonctionnent les Custom Hooks en React.

Dans la leçon précédente, nous avons appris les bases des hooks personnalisés - useFetch, useInterval, useLocalStorage et useToggle. Maintenant, nous allons plonger dans les modèles avancés, les règles des hooks et la composition qui rendront tes hooks vraiment professionnels.

Les règles des hooks

Avant de commencer à créer des hooks avancés, nous devons connaître deux règles fondamentales, dont la violation provoque des erreurs imprévisibles :

Règle 1 : N'appelle les hooks qu'au niveau supérieur

Les hooks doivent toujours être appelés dans le même ordre à chaque rendu. Ne place jamais de hooks à l'intérieur de conditions, de boucles ou de fonctions imbriquées :

1// MAUVAIS - hook à l'intérieur d'une condition
2function SpaceShip({ hasRadar }) {
3  if (hasRadar) {
4    const [signal, setSignal] = useState(null); // ERREUR !
5  }
6}
7
8// BON - condition à l'intérieur du hook
9function SpaceShip({ hasRadar }) {
10  const [signal, setSignal] = useState(null);
11
12  useEffect(() => {
13    if (hasRadar) {
14      // logique du radar
15    }
16  }, [hasRadar]);
17}

React suit les hooks en fonction de leur ordre d'appel. Si une condition fait qu'un hook est sauté, tous les hooks suivants auront des valeurs incorrectes - comme si tous les modules du vaisseau spatial changeaient de position.

Règle 2 : N'appelle les hooks que depuis des composants React ou des hooks personnalisés

Tu ne peux pas appeler de hooks depuis des fonctions JavaScript ordinaires. Uniquement depuis :

  • Les composants fonctionnels React
  • Les Custom Hooks (fonctions commençant par use)
1// MAUVAIS - fonction ordinaire
2function calculateTrajectory() {
3  const [speed] = useState(0); // ERREUR !
4}
5
6// BON - hook personnalisé
7function useTrajectory() {
8  const [speed, setSpeed] = useState(0);
9  return { speed, setSpeed };
10}

useDebounce - retarder les actions

Le hook useDebounce est extrêmement utile pour la recherche en temps réel. Au lieu d'envoyer une requête API après chaque frappe, nous attendons que l'utilisateur ait fini de taper :

1function useDebounce(value, delay) {
2  const [debouncedValue, setDebouncedValue] = useState(value);
3
4  useEffect(() => {
5    const timer = setTimeout(() => {
6      setDebouncedValue(value);
7    }, delay);
8
9    return () => clearTimeout(timer);
10  }, [value, delay]);
11
12  return debouncedValue;
13}
14
15// Utilisation - recherche de planètes
16function PlanetSearch() {
17  const [query, setQuery] = useState('');
18  const debouncedQuery = useDebounce(query, 500);
19
20  useEffect(() => {
21    if (debouncedQuery) {
22      // Requête envoyée uniquement après 500 ms de pause dans la saisie
23      fetch(`/api/planets?search=${debouncedQuery}`);
24    }
25  }, [debouncedQuery]);
26
27  return (
28    <input
29      value={query}
30      onChange={e => setQuery(e.target.value)}
31      placeholder="Rechercher des planètes..."
32    />
33  );
34}

La minuterie est réinitialisée à chaque changement de valeur. Ce n'est que lorsque l'utilisateur arrête de taper pendant 500 ms que la valeur sera mise à jour et la requête API envoyée.

usePrevious - mémoriser la valeur précédente

Ce hook simple permet de comparer la valeur actuelle avec la précédente :

1function usePrevious(value) {
2  const ref = useRef();
3
4  useEffect(() => {
5    ref.current = value;
6  }, [value]);
7
8  return ref.current;
9}
10
11// Utilisation - surveiller les changements de vitesse
12function SpeedMonitor() {
13  const [speed, setSpeed] = useState(0);
14  const previousSpeed = usePrevious(speed);
15
16  const trend = speed > previousSpeed
17    ? 'Accélération'
18    : speed < previousSpeed
19    ? 'Décélération'
20    : 'Vitesse constante';
21
22  return <p>{trend} : {speed} km/s (était : {previousSpeed})</p>;
23}

useArray - gérer les tableaux

Les tableaux nécessitent des opérations spécifiques (ajout, suppression, filtrage). Un hook personnalisé encapsule ces opérations dans une interface propre :

1function useArray(initialArray = []) {
2  const [array, setArray] = useState(initialArray);
3
4  const push = (element) => setArray(a => [...a, element]);
5  const removeByIndex = (index) =>
6    setArray(a => a.filter((_, i) => i !== index));
7  const update = (index, newElement) =>
8    setArray(a => a.map((item, i) => (i === index ? newElement : item)));
9  const clear = () => setArray([]);
10  const filter = (fn) => setArray(a => a.filter(fn));
11
12  return { array, set: setArray, push, removeByIndex, update, clear, filter };
13}
14
15// Utilisation - liste d'équipage
16function CrewManager() {
17  const crew = useArray(['Capitaine Nova', 'Navigateur Rex']);
18
19  return (
20    <div>
21      {crew.array.map((member, i) => (
22        <div key={i}>
23          {member}
24          <button onClick={() => crew.removeByIndex(i)}>Retirer</button>
25        </div>
26      ))}
27      <button onClick={() => crew.push('Nouveau membre')}>
28        Ajouter un membre d'équipage
29      </button>
30      <button onClick={crew.clear}>Renvoyer l'équipage</button>
31    </div>
32  );
33}

Composition de hooks - combiner les modules

La véritable puissance des hooks personnalisés se révèle lorsque nous les combinons entre eux. Les hooks peuvent appeler d'autres hooks, construisant des comportements complexes à partir de blocs simples :

1// Hook 1 : Hook d'état simple avec validation
2function useValidatedInput(initialValue, validator) {
3  const [value, setValue] = useState(initialValue);
4  const [error, setError] = useState(null);
5
6  const handleChange = (newValue) => {
7    setValue(newValue);
8    const validationError = validator(newValue);
9    setError(validationError);
10  };
11
12  return { value, error, onChange: handleChange, isValid: !error };
13}
14
15// Hook 2 : Composition - formulaire avec plusieurs champs
16function useSpaceshipForm() {
17  const name = useValidatedInput('', (v) =>
18    v.length < 3 ? 'Min. 3 caractères' : null
19  );
20  const fuel = useValidatedInput(100, (v) =>
21    v < 0 || v > 100 ? 'Plage 0-100' : null
22  );
23
24  const isFormValid = name.isValid && fuel.isValid;
25
26  const reset = () => {
27    name.onChange('');
28    fuel.onChange(100);
29  };
30
31  return { name, fuel, isFormValid, reset };
32}
33
34// Utilisation
35function ShipRegistration() {
36  const form = useSpaceshipForm();
37
38  return (
39    <form>
40      <input
41        value={form.name.value}
42        onChange={e => form.name.onChange(e.target.value)}
43      />
44      {form.name.error && <span>{form.name.error}</span>}
45
46      <input
47        type="number"
48        value={form.fuel.value}
49        onChange={e => form.fuel.onChange(Number(e.target.value))}
50      />
51      {form.fuel.error && <span>{form.fuel.error}</span>}
52
53      <button disabled={!form.isFormValid}>Enregistrer le vaisseau</button>
54    </form>
55  );
56}

useSpaceshipForm utilise useValidatedInput deux fois - chaque appel crée une instance indépendante avec son propre état.

Modèle : useToggle, useCounter, useForm

Modèles populaires de hooks personnalisés à connaître :

HookCas d'usageRetourne
useToggleBasculer activé/désactivé{ value, toggle, setTrue, setFalse }
useCounterCompteur avec min/max{ count, increment, decrement, reset }
useFetchRécupération de données{ data, loading, error, refetch }
useDebounceRetarder des valeursValeur retardée
usePreviousValeur précédenteValeur précédente
useArrayOpérations sur tableaux{ array, push, remove, update, clear }
useLocalStorageÉtat persistant[value, setValue]

Résumé

Les Custom Hooks sont le fondement d'une application React bien organisée :

  1. Règles des hooks - toujours au niveau supérieur, uniquement dans les composants et les hooks
  2. Préfixe use - obligatoire, permet à React de vérifier les règles
  3. Composition - les hooks peuvent appeler d'autres hooks, construisant des comportements complexes
  4. Instances indépendantes - chaque appel de hook a son propre état isolé
  5. Réutilisabilité - une logique, plusieurs composants

Comme les systèmes modulaires d'un vaisseau spatial - chaque hook est un module autonome qui peut être installé, configuré et connecté avec d'autres modules dans n'importe quelle configuration.

Code de cette leçon : App.jsx
1import React, { useState, useEffect, useRef } from 'react';
2
3// === useDebounce ===
4function useDebounce(value, delay) {
5  const [debouncedValue, setDebouncedValue] = useState(value);
6
7  useEffect(() => {
8    const timer = setTimeout(() => setDebouncedValue(value), delay);
9    return () => clearTimeout(timer);
10  }, [value, delay]);
11
12  return debouncedValue;
13}
14
15// === usePrevious ===
16function usePrevious(value) {
17  const ref = useRef();
18  useEffect(() => { ref.current = value; }, [value]);
19  return ref.current;
20}
21
22// === useToggle ===
23function useToggle(initial = false) {
24  const [value, setValue] = useState(initial);
25  const toggle = () => setValue(v => !v);
26  const setTrue = () => setValue(true);
27  const setFalse = () => setValue(false);
28  return { value, toggle, setTrue, setFalse };
29}
30
31// === useArray ===
32function useArray(initialArray = []) {
33  const [array, setArray] = useState(initialArray);
34  const push = (el) => setArray(a => [...a, el]);
35  const removeByIndex = (i) => setArray(a => a.filter((_, idx) => idx !== i));
36  const clear = () => setArray([]);
37  return { array, push, removeByIndex, clear, set: setArray };
38}
39
40// === useLocalStorage ===
41function useLocalStorage(key, initialValue) {
42  const [storedValue, setStoredValue] = useState(() => {
43    try {
44      const item = window.localStorage.getItem(key);
45      return item ? JSON.parse(item) : initialValue;
46    } catch { return initialValue; }
47  });
48
49  useEffect(() => {
50    try { window.localStorage.setItem(key, JSON.stringify(storedValue)); }
51    catch (e) { console.error(e); }
52  }, [key, storedValue]);
53
54  return [storedValue, setStoredValue];
55}
56
57// --- Demo Components ---
58
59function DebounceDemo() {
60  const [text, setText] = useState('');
61  const debouncedText = useDebounce(text, 600);
62  const [results, setResults] = useState([]);
63
64  useEffect(() => {
65    if (debouncedText) {
66      const planets = ['Mercure','Vénus','Terre','Mars','Jupiter','Saturne','Uranus','Neptune'];
67      setResults(planets.filter(p =>
68        p.toLowerCase().includes(debouncedText.toLowerCase())
69      ));
70    } else {
71      setResults([]);
72    }
73  }, [debouncedText]);
74
75  return (
76    <div className="demo-card">
77      <h3>useDebounce</h3>
78      <input value={text} onChange={e => setText(e.target.value)} placeholder="Rechercher des planètes..." />
79      <p className="hint">Debounced: "{debouncedText}"</p>
80      {results.length > 0 && (
81        <ul className="results">{results.map(r => <li key={r}>{r}</li>)}</ul>
82      )}
83    </div>
84  );
85}
86
87function PreviousDemo() {
88  const [speed, setSpeed] = useState(0);
89  const prevSpeed = usePrevious(speed);
90
91  const trend = speed > (prevSpeed || 0) ? 'Accélération' :
92                speed < (prevSpeed || 0) ? 'Décélération' : 'Vitesse stable';
93
94  return (
95    <div className="demo-card">
96      <h3>usePrevious</h3>
97      <p className="speed-display">{speed} km/s</p>
98      <p className="hint">Précédemment : {prevSpeed ?? 0} km/s | {trend}</p>
99      <div className="btn-row">
100        <button onClick={() => setSpeed(s => s + 10)}>+10</button>
101        <button onClick={() => setSpeed(s => Math.max(0, s - 10))}>-10</button>
102        <button onClick={() => setSpeed(0)}>Reset</button>
103      </div>
104    </div>
105  );
106}
107
108function ToggleDemo() {
109  const shields = useToggle(false);
110  const engines = useToggle(true);
111
112  return (
113    <div className="demo-card">
114      <h3>useToggle</h3>
115      <div className="toggle-item">
116        <span>Boucliers : <b style={{color: shields.value ? '#2ecc71' : '#e74c3c'}}>
117          {shields.value ? 'ACTIFS' : 'DÉSACTIVÉS'}
118        </b></span>
119        <button onClick={shields.toggle}>Basculer</button>
120      </div>
121      <div className="toggle-item">
122        <span>Moteurs : <b style={{color: engines.value ? '#2ecc71' : '#e74c3c'}}>
123          {engines.value ? 'ACTIFS' : 'DÉSACTIVÉS'}
124        </b></span>
125        <button onClick={engines.toggle}>Basculer</button>
126      </div>
127    </div>
128  );
129}
130
131function ArrayDemo() {
132  const crew = useArray(['Capitaine Nova', 'Navigateur Rex']);
133  const [name, setName] = useState('');
134
135  const addMember = () => {
136    if (name.trim()) { crew.push(name.trim()); setName(''); }
137  };
138
139  return (
140    <div className="demo-card">
141      <h3>useArray</h3>
142      <div className="crew-list">
143        {crew.array.map((member, i) => (
144          <div key={i} className="crew-member">
145            <span>{member}</span>
146            <button onClick={() => crew.removeByIndex(i)} className="btn-small">X</button>
147          </div>
148        ))}
149      </div>
150      <div className="add-row">
151        <input value={name} onChange={e => setName(e.target.value)}
152          placeholder="Nom du membre d'équipage..." onKeyDown={e => e.key === 'Enter' && addMember()} />
153        <button onClick={addMember}>Ajouter</button>
154      </div>
155      <button onClick={crew.clear} className="btn-danger">Renvoyer l'équipage</button>
156    </div>
157  );
158}
159
160function StorageDemo() {
161  const [theme, setTheme] = useLocalStorage('space-theme', 'dark');
162  const [volume, setVolume] = useLocalStorage('space-volume', 50);
163
164  return (
165    <div className="demo-card">
166      <h3>useLocalStorage</h3>
167      <div className="setting">
168        <label>Thème :</label>
169        <select value={theme} onChange={e => setTheme(e.target.value)}>
170          <option value="dark">Sombre</option>
171          <option value="light">Clair</option>
172          <option value="cosmic">Cosmique</option>
173        </select>
174      </div>
175      <div className="setting">
176        <label>Volume : {volume}%</label>
177        <input type="range" min="0" max="100" value={volume}
178          onChange={e => setVolume(Number(e.target.value))} />
179      </div>
180      <p className="hint">Valeurs enregistrées dans localStorage</p>
181    </div>
182  );
183}
184
185export default function App() {
186  return (
187    <div className="app">
188      <h1>Custom Hooks - Modules du vaisseau spatial</h1>
189      <div className="grid">
190        <DebounceDemo />
191        <PreviousDemo />
192        <ToggleDemo />
193        <ArrayDemo />
194        <StorageDemo />
195      </div>
196    </div>
197  );
198}

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. Pourquoi ne peux-tu PAS appeler des hooks à l'intérieur d'instructions conditionnelles (if/else) ?

  2. 2. En quoi un hook personnalisé diffère-t-il d'une fonction JavaScript ordinaire ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Crée un hook personnalisé useDebounce(value, delay) retournant une valeur débouncée et useToggle(initialValue) retournant { value, toggle, setTrue, setFalse }. Les deux hooks doivent fonctionner avec les composants de test sur la page.

  • Tri vertical

    Ordonne les étapes pour créer un hook personnalisé du début à la fin :

  • Éditeur de code

    Crée un hook personnalisé useArray(initialArray) retournant { array, set, push, removeByIndex, update, clear, filter } et usePrevious(value) retournant la valeur précédente. Les hooks doivent fonctionner avec le composant de gestion d'équipage sur la page.

  • Clique dans l'ordre

    Ordonne la syntaxe du hook personnalisé useDebounce dans le bon ordre :

  • Éditeur de code

    Construis un panneau de contrôle complet de l'observatoire des anneaux de Saturne contenant : useEffect pour surveiller les différentes phases orbitales des anneaux, useLayoutEffect pour la synchronisation des mesures en temps réel, des fonctions de nettoyage pour le démontage correct des capteurs, des tableaux de dépendances pour un contrôle précis de la ré-exécution, des effets multiples pour des systèmes de surveillance indépendants (surveillance RingA, RingB, RingC), des modèles d'optimisation d'effets, des effets async pour la récupération de données depuis des sondes spatiales, et des effets conditionnels réagissant aux différents états de l'observatoire. Crée un système de surveillance digne de l'observatoire des anneaux de Saturne le plus avancé !

Articles utiles