Cours JavaScript et React · Module 5: useEffect et hooks personnalisés
Stocker des valeurs entre les rendus avec useRef
Dans cette leçon6
Imagine la situation : tu veux mémoriser l'identifiant du minuteur de mission ou la vitesse précédente du vaisseau. Une simple variable dans le composant ne convient pas, car à chaque rendu la fonction du composant s'exécute de nouveau et la variable revient à sa valeur initiale. useState mémoriserait la valeur, mais chaque modification provoquerait un nouveau rendu, alors que l'écran n'a pas du tout besoin de changer. Il nous faut quelque chose entre les deux.
Dans notre voyage cosmique à travers React, nous devons parfois stocker des informations qui persistent entre les rendus du composant mais ne devraient pas provoquer de nouveau rendu. Le hook useRef est comme une capsule de sauvetage - il stocke des données que tu peux récupérer à tout moment, peu importe combien de fois ton vaisseau spatial (le composant) est reconstruit.
Qu'est-ce que useRef ?
Le hook useRef crée un objet de référence mutable dont la propriété .current est initialisée avec la valeur passée. Une caractéristique importante de useRef est que :
- La valeur stockée dans la référence reste la même entre les rendus
- Changer la valeur de la ref ne provoque PAS de nouveau rendu du composant
- Agit comme un « espace mémoire » que tu peux référencer à tout moment du cycle de vie du composant
La documentation react.dev ajoute deux détails. Premièrement, useRef renvoie le même objet à chaque rendu, et la valeur initiale n'est utilisée qu'au premier rendu. Deuxièmement, tu modifies .current par une simple affectation, sans aucune fonction setter.
useRef vs useState - différences clés
Comparons deux méthodes clés de stockage de données dans les composants. Dans le tableau de bord ci-dessous, la vitesse est gardée dans l'état et la vitesse précédente dans une ref, que useEffect met à jour après chaque changement :
1function SpaceshipDashboard() {
2 // État - les changements provoquent un nouveau rendu
3 const [speed, setSpeed] = useState(0);
4
5 // Référence - les changements ne provoquent PAS de nouveau rendu
6 const previousSpeedRef = useRef(0);
7
8 // Lorsque speed change, nous sauvegardons la valeur précédente
9 useEffect(() => {
10 previousSpeedRef.current = speed;
11 }, [speed]);
12
13 const increaseSpeed = () => {
14 setSpeed(speed + 10); // Cela provoquera un nouveau rendu
15 };
16
17 return (
18 <div>
19 <p>Vitesse actuelle : {speed} km/h</p>
20 <p>Vitesse précédente : {previousSpeedRef.current} km/h</p>
21 <button onClick={increaseSpeed}>Augmenter la vitesse</button>
22 </div>
23 );
24}Un clic modifie l'état, donc le composant effectue un nouveau rendu. Pendant ce rendu, la ref contient encore l'ancienne valeur, car l'effet ne s'exécute qu'après le rendu. C'est pour cela que tu vois la vitesse précédente à l'écran. L'affectation à previousSpeedRef.current dans l'effet ne rafraîchit rien par elle-même.
Une réserve importante : react.dev recommande de ne pas lire ni écrire ref.current pendant le rendu (l'initialisation paresseuse est l'exception). Ce modèle fonctionne dans un tableau de bord simple, mais il est fragile. Si la valeur précédente doit être visible à l'écran, la documentation conseille de la garder dans l'état. Réserve les refs aux données que l'écran n'affiche pas.
Cas d'usage courants de useRef
1. Stocker les valeurs précédentes
L'une des utilisations les plus populaires de useRef est de mémoriser les valeurs précédentes entre les rendus. Le traqueur d'astéroïde calcule de combien l'objet s'est déplacé depuis le dernier rendu :
1function AsteroidTracker({ asteroid }) {
2 const [position, setPosition] = useState({ x: 0, y: 0 });
3 const previousPositionRef = useRef({ x: 0, y: 0 });
4
5 useEffect(() => {
6 // Nous sauvegardons la position actuelle comme précédente avant la prochaine mise à jour
7 previousPositionRef.current = position;
8 }, [position]);
9
10 // Nous calculons la distance parcourue par l'astéroïde
11 const distanceMoved = Math.sqrt(
12 Math.pow(position.x - previousPositionRef.current.x, 2) +
13 Math.pow(position.y - previousPositionRef.current.y, 2)
14 );
15
16 return (
17 <div>
18 <p>Position actuelle : ({position.x}, {position.y})</p>
19 <p>Position précédente : ({previousPositionRef.current.x}, {previousPositionRef.current.y})</p>
20 <p>Distance parcourue : {distanceMoved}</p>
21 </div>
22 );
23}Le mécanisme est le même que dans le tableau de bord de vitesse : l'effet enregistre la position après le rendu, et le calcul utilise la valeur du rendu précédent. La même réserve sur la lecture des refs pendant le rendu s'applique ici.
2. Compteurs et valeurs d'instance qui n'affectent pas le rendu
C'est ici que useRef brille vraiment. Le temps de mission et l'identifiant de l'intervalle sont des données qui changent chaque seconde mais ne sont pas affichées en direct :
1function MissionControl() {
2 const missionTimeRef = useRef(0);
3 const timerIdRef = useRef(null);
4
5 // Démarrer la minuterie de mission
6 const startMissionTimer = () => {
7 if (timerIdRef.current !== null) return; // Éviter les démarrages multiples
8
9 timerIdRef.current = setInterval(() => {
10 missionTimeRef.current += 1;
11 console.log(`Temps de mission : ${missionTimeRef.current} secondes`);
12 // Note que cela ne provoque PAS de nouveau rendu du composant
13 }, 1000);
14 };
15
16 // Arrêter la minuterie de mission
17 const stopMissionTimer = () => {
18 clearInterval(timerIdRef.current);
19 timerIdRef.current = null;
20 };
21
22 // Nettoyage au démontage du composant
23 useEffect(() => {
24 return () => {
25 if (timerIdRef.current !== null) {
26 clearInterval(timerIdRef.current);
27 }
28 };
29 }, []);
30
31 return (
32 <div>
33 <button onClick={startMissionTimer}>Démarrer la mission</button>
34 <button onClick={stopMissionTimer}>Terminer la mission</button>
35 <button onClick={() => alert(`Temps de mission : ${missionTimeRef.current} secondes`)}>
36 Afficher le temps de mission
37 </button>
38 </div>
39 );
40}Le compteur augmente chaque seconde, la console le confirme, et le composant ne fait pas un seul nouveau rendu. On lit les refs uniquement dans les gestionnaires d'événements et dans l'effet, exactement là où la documentation le recommande. Les boutons et leur apparence restent inchangés pendant toute la mission.
3. Accéder aux éléments du DOM
La troisième utilisation, c'est une poignée vers un vrai élément de la page. Tu passes la ref dans l'attribut ref, et après le rendu React enregistre le nœud DOM dans .current :
1function SpaceshipScanner() {
2 const scannerInputRef = useRef(null);
3
4 const focusScannerInput = () => {
5 // Accès direct à l'élément DOM
6 scannerInputRef.current.focus();
7 };
8
9 return (
10 <div>
11 <input
12 ref={scannerInputRef}
13 type="text"
14 placeholder="Saisis les coordonnées..."
15 />
16 <button onClick={focusScannerInput}>Activer le scanner</button>
17 </div>
18 );
19}Un clic sur le bouton place le curseur dans le champ des coordonnées. React ne crée pas de nouveau champ ici, il te donne seulement accès au champ existant. Avec React 19, tu peux aussi passer ref comme une prop ordinaire à ton propre composant fonction, sans forwardRef.
useRef et useCallback
Les deux hooks « mémorisent » quelque chose, mais pas la même chose. useRef garde n'importe quelle valeur que tu modifies toi-même. useCallback mémorise une fonction et n'en crée une nouvelle que lorsqu'une de ses dépendances change :
1const handleClick = useCallback(() => {
2 onClick(id);
3}, [id, onClick]);L'ordre est toujours le même : la fonction, puis le tableau de dépendances. Tant que id et onClick ne changent pas, handleClick reste le même objet fonction.
Souviens-toi des règles d'utilisation de useRef
- N'en abuse pas : N'utilise useRef que lorsque la valeur ne doit vraiment pas provoquer de nouveau rendu
- Évite les mutations directes du DOM : Bien que useRef te donne accès aux éléments du DOM, utilise-le avec parcimonie et conformément à la philosophie de React
- Souviens-toi de nettoyer : Si tu utilises useRef pour stocker des minuteries ou des abonnements, souviens-toi de les nettoyer dans la fonction de nettoyage de useEffect
Mon conseil : avant d'utiliser une ref, demande-toi si la valeur doit être visible à l'écran. Si oui, utilise l'état. Sinon, une ref sera plus légère.
Résumé
Le hook useRef est inestimable lorsque tu dois :
- Stocker une valeur entre les rendus sans provoquer de nouveau rendu
- Suivre les valeurs précédentes
- Obtenir un accès direct aux éléments du DOM
- Stocker des identifiants de minuteries ou d'autres valeurs d'instance
Dans la prochaine leçon, nous construirons nos propres hooks, dans lesquels les refs cachent souvent des détails comme les minuteurs.
Comme une capsule de sauvetage dans l'espace, useRef stocke des données précieuses qui peuvent être nécessaires à des moments critiques de la mission de ton composant.
Code de cette leçon : App.jsx
1import React, { useState, useRef, useEffect } from 'react';
2
3// Suivi des valeurs précédentes
4function PreviousValueTracker() {
5 const [position, setPosition] = useState({ x: 0, y: 0 });
6 const prevPositionRef = useRef({ x: 0, y: 0 });
7
8 useEffect(() => {
9 prevPositionRef.current = position;
10 });
11
12 const move = (dx, dy) => {
13 setPosition(prev => ({
14 x: Math.max(-100, Math.min(100, prev.x + dx)),
15 y: Math.max(-100, Math.min(100, prev.y + dy))
16 }));
17 };
18
19 return (
20 <div className="demo-card">
21 <h2>Suivi de position</h2>
22 <div className="position-display">
23 <div className="current">
24 <h3>Actuelle</h3>
25 <p>X: {position.x}</p>
26 <p>Y: {position.y}</p>
27 </div>
28 <div className="previous">
29 <h3>Précédente</h3>
30 <p>X: {prevPositionRef.current.x}</p>
31 <p>Y: {prevPositionRef.current.y}</p>
32 </div>
33 </div>
34 <div className="controls">
35 <button onClick={() => move(0, -10)}>UP</button>
36 <div className="horizontal">
37 <button onClick={() => move(-10, 0)}>LEFT</button>
38 <button onClick={() => move(10, 0)}>RIGHT</button>
39 </div>
40 <button onClick={() => move(0, 10)}>DOWN</button>
41 </div>
42 </div>
43 );
44}
45
46// Compteur de rendus
47function RenderCounter() {
48 const renderCountRef = useRef(0);
49 const [value, setValue] = useState('');
50
51 renderCountRef.current++;
52
53 return (
54 <div className="demo-card">
55 <h2>Compteur de rendus</h2>
56 <div className="render-count">
57 Rendus : <span>{renderCountRef.current}</span>
58 </div>
59 <input
60 value={value}
61 onChange={(e) => setValue(e.target.value)}
62 placeholder="Tape pour déclencher un rendu..."
63 />
64 <p className="hint">
65 useRef stocke une valeur sans provoquer de nouveau rendu
66 </p>
67 </div>
68 );
69}
70
71// Comparaison d'objets
72function ObjectComparisonDemo() {
73 const [user, setUser] = useState({ name: 'Astronaute', level: 1 });
74 const prevUserRef = useRef(user);
75 const [changes, setChanges] = useState([]);
76
77 useEffect(() => {
78 const changedFields = [];
79
80 if (prevUserRef.current.name !== user.name) {
81 changedFields.push(`name: "${prevUserRef.current.name}" -> "${user.name}"`);
82 }
83 if (prevUserRef.current.level !== user.level) {
84 changedFields.push(`level: ${prevUserRef.current.level} -> ${user.level}`);
85 }
86
87 if (changedFields.length > 0) {
88 setChanges(prev => [...prev.slice(-4), ...changedFields]);
89 }
90
91 prevUserRef.current = user;
92 }, [user]);
93
94 return (
95 <div className="demo-card">
96 <h2>Comparaison des changements</h2>
97 <div className="user-info">
98 <input
99 value={user.name}
100 onChange={(e) => setUser(prev => ({ ...prev, name: e.target.value }))}
101 placeholder="Prénom"
102 />
103 <div className="level-controls">
104 <button onClick={() => setUser(prev => ({ ...prev, level: Math.max(1, prev.level - 1) }))}>
105 -
106 </button>
107 <span>Level: {user.level}</span>
108 <button onClick={() => setUser(prev => ({ ...prev, level: prev.level + 1 }))}>
109 +
110 </button>
111 </div>
112 </div>
113 <div className="changes">
114 <h4>Historique des changements :</h4>
115 {changes.map((change, i) => (
116 <div key={i} className="change">{change}</div>
117 ))}
118 </div>
119 </div>
120 );
121}
122
123// Cache des valeurs calculées
124function ComputationCacheDemo() {
125 const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]);
126 const [multiplier, setMultiplier] = useState(2);
127 const cacheRef = useRef(new Map());
128 const cacheHits = useRef(0);
129 const cacheMisses = useRef(0);
130
131 const computeExpensive = (num, mult) => {
132 const key = `${num}-${mult}`;
133
134 if (cacheRef.current.has(key)) {
135 cacheHits.current += 1;
136 return cacheRef.current.get(key);
137 }
138
139 // Simulation d'une opération coûteuse
140 const result = num * mult;
141 cacheRef.current.set(key, result);
142 cacheMisses.current += 1;
143 return result;
144 };
145
146 const results = numbers.map(n => computeExpensive(n, multiplier));
147
148 const addNumber = () => {
149 setNumbers(prev => [...prev, prev.length + 1]);
150 };
151
152 return (
153 <div className="demo-card">
154 <h2>Cache avec useRef</h2>
155 <div className="cache-stats">
156 <span className="hit">Succès : {cacheHits.current}</span>
157 <span className="miss">Échecs : {cacheMisses.current}</span>
158 </div>
159 <div className="multiplier">
160 <button onClick={() => setMultiplier(m => Math.max(1, m - 1))}>-</button>
161 <span>Multiplicateur : {multiplier}</span>
162 <button onClick={() => setMultiplier(m => m + 1)}>+</button>
163 </div>
164 <div className="numbers">
165 {numbers.map((n, i) => (
166 <div key={i} className="number-result">
167 {n} x {multiplier} = {results[i]}
168 </div>
169 ))}
170 </div>
171 <button onClick={addNumber}>+ Ajouter un nombre</button>
172 </div>
173 );
174}
175
176function App() {
177 return (
178 <div className="app">
179 <h1>Stockage de valeurs avec useRef</h1>
180 <div className="demos">
181 <PreviousValueTracker />
182 <RenderCounter />
183 <ObjectComparisonDemo />
184 <ComputationCacheDemo />
185 </div>
186 </div>
187 );
188}
189
190export default App;Tu as repéré une erreur dans cette leçon ?
Tâches pratiques dans le jeu
- Tri horizontal
Ordonne la syntaxe correcte de useCallback pour un gestionnaire d'événements :