Cours JavaScript et React · Module 5: useEffect et hooks personnalisés
Optimisation des performances avec useEffect
Dans cette leçon7
Dans notre voyage cosmique à travers React, les performances sont d'une importance critique - tout comme la gestion des ressources sur un vaisseau spatial. Une utilisation sous-optimale des effets de bord peut entraîner des re-rendus excessifs, un gaspillage de ressources et une réponse lente de l'application. Dans ce module, nous allons apprendre à optimiser useEffect pour que nos applications fonctionnent de façon fluide et efficace.
Problèmes de performance avec useEffect
Le hook useEffect est un outil puissant, mais lorsqu'il est mal utilisé, il peut causer divers problèmes :
- Effets qui s'exécutent trop fréquemment - lorsque les dépendances changent souvent ou sont mal définies
- Opérations coûteuses - lors de l'exécution de calculs complexes ou de manipulations du DOM à l'intérieur des effets
- Mises à jour en cascade - lorsqu'un effet provoque des changements d'état qui déclenchent des effets ultérieurs
- Re-rendus inutiles - lorsque les effets mettent à jour l'état même si les données n'ont pas réellement changé
Optimiser le tableau de dépendances
Le tableau de dépendances de useEffect est la clé de l'optimisation - il détermine quand l'effet doit être ré-exécuté.
Erreur 1 : Trop de dépendances
1// Code problématique
2function AstronautTracker({ team }) {
3 const [position, setPosition] = useState({ x: 0, y: 0 });
4 const [status, setStatus] = useState('online');
5
6 // Cet effet va s'exécuter trop fréquemment
7 useEffect(() => {
8 console.log(`Suivi de l'astronaute : ${team.leader}`);
9 fetchAstronautPosition(team.leader).then(newPosition => {
10 setPosition(newPosition);
11 });
12 }, [team, position, status]); // Dépendances inutiles : position et status
13
14 return <div>Position du chef : {position.x}, {position.y}</div>;
15}Solution : N'utilise que les dépendances nécessaires
1// Code corrigé
2function AstronautTracker({ team }) {
3 const [position, setPosition] = useState({ x: 0, y: 0 });
4 const [status, setStatus] = useState('online');
5
6 // L'effet n'utilise que les dépendances nécessaires
7 useEffect(() => {
8 console.log(`Suivi de l'astronaute : ${team.leader}`);
9 fetchAstronautPosition(team.leader).then(newPosition => {
10 setPosition(newPosition);
11 });
12 }, [team.leader]); // Seule la dépendance nécessaire
13
14 return <div>Position du chef : {position.x}, {position.y}</div>;
15}Technique : Mémoïser les objets et les fonctions
Les objets et les fonctions créés pendant le rendu sont toujours « nouveaux » du point de vue de la comparaison des dépendances. Nous pouvons optimiser cela à l'aide des hooks useMemo et useCallback :
1function SpaceshipDashboard() {
2 // Sans optimisation - l'objet config est recréé à chaque rendu
3 const config = { speed: 10, fuel: 100 };
4
5 // Avec optimisation - l'objet config est mémoïsé et réutilisé
6 const memoizedConfig = useMemo(() => ({ speed: 10, fuel: 100 }), []);
7
8 // Sans optimisation - la fonction handleData est recréée à chaque rendu
9 const handleData = (data) => console.log(data);
10
11 // Avec optimisation - la fonction handleData est mémoïsée et réutilisée
12 const memoizedHandleData = useCallback((data) => console.log(data), []);
13
14 useEffect(() => {
15 // Cet effet ne s'exécutera qu'une seule fois
16 const subscription = spaceApi.subscribe(memoizedConfig, memoizedHandleData);
17 return () => subscription.unsubscribe();
18 }, [memoizedConfig, memoizedHandleData]); // Les dépendances sont stables
19
20 return <div>Vaisseau spatial</div>;
21}Techniques d'optimisation des opérations coûteuses
1. Debouncing et throttling
Lorsqu'un effet réagit à des valeurs qui changent rapidement (ex. la saisie de l'utilisateur), il vaut la peine d'utiliser le debouncing ou le throttling :
1function SearchAsteroids({ query }) {
2 const [results, setResults] = useState([]);
3 const [debouncedQuery, setDebouncedQuery] = useState(query);
4
5 // Debouncing de la requête - met à jour au plus toutes les 500 ms
6 useEffect(() => {
7 const timeoutId = setTimeout(() => {
8 setDebouncedQuery(query);
9 }, 500);
10
11 return () => clearTimeout(timeoutId);
12 }, [query]);
13
14 // L'effet de recherche s'exécute uniquement lorsque debouncedQuery change
15 useEffect(() => {
16 if (debouncedQuery) {
17 searchAsteroids(debouncedQuery).then(setResults);
18 }
19 }, [debouncedQuery]);
20
21 return (
22 <div>
23 <h2>Résultats de recherche</h2>
24 <ul>
25 {results.map(asteroid => (
26 <li key={asteroid.id}>{asteroid.name}</li>
27 ))}
28 </ul>
29 </div>
30 );
31}2. Éviter les mises à jour d'état inutiles
1function SpaceStation({ stationId }) {
2 const [data, setData] = useState(null);
3
4 useEffect(() => {
5 let isMounted = true;
6
7 fetchStationData(stationId).then(newData => {
8 // Mettre à jour uniquement si le composant est toujours monté
9 if (isMounted) {
10 // Mettre à jour uniquement si les données ont réellement changé
11 if (JSON.stringify(data) !== JSON.stringify(newData)) {
12 setData(newData);
13 }
14 }
15 });
16
17 return () => {
18 isMounted = false;
19 };
20 }, [stationId]);
21
22 return data ? <StationDisplay data={data} /> : <Loading />;
23}Optimiser les effets avec des données asynchrones
Problème : Race conditions
Lorsqu'un effet effectue des opérations asynchrones, des « race conditions » peuvent survenir - des situations où une requête plus récente se termine avant une plus ancienne :
1// Code problématique - sensible aux race conditions
2function AsteroidDetails({ asteroidId }) {
3 const [details, setDetails] = useState(null);
4
5 useEffect(() => {
6 fetchAsteroidDetails(asteroidId).then(response => {
7 setDetails(response); // Possible race condition si asteroidId change rapidement
8 });
9 }, [asteroidId]);
10
11 return <div>{details?.name}</div>;
12}Solution : Annuler les requêtes obsolètes
1// Code corrigé - protégé contre les race conditions
2function AsteroidDetails({ asteroidId }) {
3 const [details, setDetails] = useState(null);
4
5 useEffect(() => {
6 let isCurrent = true;
7
8 fetchAsteroidDetails(asteroidId).then(response => {
9 // Mettre à jour l'état uniquement si c'est toujours la requête actuelle
10 if (isCurrent) {
11 setDetails(response);
12 }
13 });
14
15 return () => {
16 isCurrent = false; // Marquer que cet effet n'est plus actuel
17 };
18 }, [asteroidId]);
19
20 return <div>{details?.name}</div>;
21}Technique avancée : AbortController
Pour les API qui prennent en charge AbortController (ex. fetch), nous pouvons réellement annuler les requêtes en cours :
1function PlanetScanner({ sector }) {
2 const [planets, setPlanets] = useState([]);
3 const [loading, setLoading] = useState(false);
4
5 useEffect(() => {
6 setLoading(true);
7
8 // Création de l'abort controller
9 const controller = new AbortController();
10 const signal = controller.signal;
11
12 fetch(`https://api.space/planets?sector=${sector}`, { signal })
13 .then(response => response.json())
14 .then(data => {
15 setPlanets(data);
16 setLoading(false);
17 })
18 .catch(error => {
19 if (error.name !== 'AbortError') {
20 console.error('Erreur :', error);
21 setLoading(false);
22 }
23 });
24
25 // La fonction de nettoyage annule la requête si l'effet est interrompu
26 return () => {
27 controller.abort();
28 };
29 }, [sector]);
30
31 return (
32 <div>
33 {loading ? <Loading /> : (
34 <PlanetsList planets={planets} />
35 )}
36 </div>
37 );
38}Séparer la logique dans des hooks personnalisés
L'une des meilleures façons d'optimiser est de créer des hooks personnalisés qui encapsulent une logique complexe et peuvent être réutilisés entre les composants :
1// Hook personnalisé pour récupérer et gérer des données
2function usePlanetData(planetId) {
3 const [data, setData] = useState(null);
4 const [loading, setLoading] = useState(true);
5 const [error, setError] = useState(null);
6
7 useEffect(() => {
8 let isMounted = true;
9 setLoading(true);
10
11 fetchPlanetData(planetId)
12 .then(result => {
13 if (isMounted) {
14 setData(result);
15 setLoading(false);
16 }
17 })
18 .catch(err => {
19 if (isMounted) {
20 setError(err);
21 setLoading(false);
22 }
23 });
24
25 return () => {
26 isMounted = false;
27 };
28 }, [planetId]);
29
30 return { data, loading, error };
31}
32
33// Utilisation du hook personnalisé
34function PlanetProfile({ planetId }) {
35 const { data, loading, error } = usePlanetData(planetId);
36
37 if (loading) return <LoadingIndicator />;
38 if (error) return <ErrorMessage error={error} />;
39
40 return (
41 <div>
42 <h1>{data.name}</h1>
43 <p>Diamètre : {data.diameter} km</p>
44 <p>Climat : {data.climate}</p>
45 </div>
46 );
47}Utiliser le mécanisme layoutEffect
Dans les rares cas où tu dois effectuer des mesures du DOM ou te synchroniser avec la mise en page de la page avant le rendu, tu peux utiliser useLayoutEffect à la place de useEffect :
1function CockpitInterface() {
2 const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
3 const cockpitRef = useRef(null);
4
5 // useLayoutEffect s'exécute de façon synchrone après toutes les mutations du DOM,
6 // mais avant le rendu à l'écran
7 useLayoutEffect(() => {
8 if (cockpitRef.current) {
9 const { width, height } = cockpitRef.current.getBoundingClientRect();
10 setDimensions({ width, height });
11 }
12 }, []);
13
14 return (
15 <div ref={cockpitRef} className="cockpit">
16 <ControlPanel width={dimensions.width} height={dimensions.height} />
17 </div>
18 );
19}Remarque :
useLayoutEffectbloque le rendu visuel, il doit donc être utilisé avec parcimonie et uniquement lorsque c'est absolument nécessaire.
Résumé : Bonnes pratiques pour optimiser useEffect
- Minimise les dépendances - n'utilise que les valeurs nécessaires dans le tableau de dépendances
- Sépare les effets - au lieu d'un effet complexe, utilise plusieurs effets plus simples
- Mémoïse les dépendances - utilise useMemo et useCallback pour les objets et les fonctions
- Utilise le debouncing/throttling - pour les valeurs qui changent rapidement
- Annule les opérations async obsolètes - évite les race conditions et les fuites de mémoire
- Crée des hooks personnalisés - encapsule la logique complexe dans des hooks réutilisables
- Analyse les schémas de rendu - utilise React DevTools pour détecter les rendus excessifs
- Évite les boucles infinies - assure-toi toujours que les effets ne provoquent pas de cycles de mise à jour infinis
En appliquant ces techniques d'optimisation, ton application React fonctionnera efficacement même avec des opérations complexes et de grands ensembles de données - tout comme un vaisseau spatial bien optimisé peut parcourir de vastes distances avec une consommation minimale de ressources.
Code de cette leçon : App.jsx
1import React, { useState, useEffect, useRef, useCallback } from 'react';
2
3// Démonstration du tableau de dépendances
4function DependencyArrayDemo() {
5 const [count, setCount] = useState(0);
6 const [name, setName] = useState('');
7 const [logs, setLogs] = useState([]);
8
9 const addLog = (msg) => {
10 setLogs(prev => [...prev.slice(-4), `[${new Date().toLocaleTimeString()}] ${msg}`]);
11 };
12
13 // Effect sans dépendances - s'exécute à chaque rendu
14 const effectRuns = useRef(0);
15 useEffect(() => {
16 // Pas de setState ici : un changement d’état provoquerait un nouveau rendu et une boucle infinie
17 effectRuns.current += 1;
18 console.log(`Effect SANS tableau de dépendances (${effectRuns.current})`);
19 });
20
21 // Effect avec un tableau vide - seulement au montage
22 useEffect(() => {
23 addLog('Effect avec [] - seulement au mount');
24 }, []);
25
26 // Effect avec dépendance sur count
27 useEffect(() => {
28 addLog(`Effect avec [count] - count = ${count}`);
29 }, [count]);
30
31 // Effect avec dépendance sur name
32 useEffect(() => {
33 if (name) addLog(`Effect avec [name] - name = "${name}"`);
34 }, [name]);
35
36 return (
37 <div className="demo-card">
38 <h2>Tableau de dépendances</h2>
39 <div className="inputs">
40 <button onClick={() => setCount(c => c + 1)}>
41 Count: {count}
42 </button>
43 <input
44 value={name}
45 onChange={(e) => setName(e.target.value)}
46 placeholder="Saisis un prénom..."
47 />
48 </div>
49 <div className="logs">
50 {logs.map((log, i) => <div key={i}>{log}</div>)}
51 </div>
52 </div>
53 );
54}
55
56// Optimisation des requêtes fetch
57function OptimizedFetchDemo() {
58 const [searchTerm, setSearchTerm] = useState('');
59 const [results, setResults] = useState([]);
60 const [loading, setLoading] = useState(false);
61 const [fetchCount, setFetchCount] = useState(0);
62 const abortControllerRef = useRef(null);
63
64 // Recherche différée (debounce) avec cleanup
65 useEffect(() => {
66 // Annule la requête précédente
67 if (abortControllerRef.current) {
68 abortControllerRef.current.abort();
69 }
70
71 if (!searchTerm.trim()) {
72 setResults([]);
73 return;
74 }
75
76 // Debounce - attends 500ms avant la recherche
77 const timeoutId = setTimeout(() => {
78 abortControllerRef.current = new AbortController();
79
80 setLoading(true);
81 setFetchCount(c => c + 1);
82
83 // Simulation d'un appel API
84 setTimeout(() => {
85 const mockResults = [
86 `Planète : ${searchTerm}-Prime`,
87 `Étoile : ${searchTerm}-Alpha`,
88 `Nébuleuse : ${searchTerm}-Nebula`,
89 ];
90 setResults(mockResults);
91 setLoading(false);
92 }, 500);
93 }, 500);
94
95 // Cleanup - annule le timeout et la requête
96 return () => {
97 clearTimeout(timeoutId);
98 if (abortControllerRef.current) {
99 abortControllerRef.current.abort();
100 }
101 };
102 }, [searchTerm]);
103
104 return (
105 <div className="demo-card">
106 <h2>Optimisation du Fetch</h2>
107 <div className="fetch-counter">
108 Requêtes effectuées : <span>{fetchCount}</span>
109 </div>
110 <input
111 value={searchTerm}
112 onChange={(e) => setSearchTerm(e.target.value)}
113 placeholder="Rechercher des objets cosmiques..."
114 />
115 <div className="results">
116 {loading ? (
117 <div className="loading">Recherche en cours...</div>
118 ) : results.length > 0 ? (
119 results.map((result, i) => (
120 <div key={i} className="result">{result}</div>
121 ))
122 ) : searchTerm ? (
123 <div className="empty">Tape pour rechercher</div>
124 ) : null}
125 </div>
126 <p className="hint">Debounce 500ms + AbortController</p>
127 </div>
128 );
129}
130
131// Éviter les boucles infinies
132function InfiniteLoopPreventionDemo() {
133 const [data, setData] = useState({ count: 0 });
134 const renderCount = useRef(0);
135 const [safeMode, setSafeMode] = useState(true);
136
137 // Comptage des rendus
138 renderCount.current += 1;
139
140 // DANGEREUX - provoquerait une boucle infinie
141 // useEffect(() => {
142 // setData({ count: data.count }); // Nouvel objet = nouvelle référence = re-render
143 // }, [data]);
144
145 // SÛR - on utilise un callback
146 const incrementSafe = useCallback(() => {
147 setData(prev => ({ count: prev.count + 1 }));
148 }, []);
149
150 return (
151 <div className="demo-card">
152 <h2>Éviter les boucles</h2>
153 <div className="stats">
154 <div className="stat">
155 <span className="label">Count:</span>
156 <span className="value">{data.count}</span>
157 </div>
158 <div className="stat">
159 <span className="label">Rendus :</span>
160 <span className="value">{renderCount.current}</span>
161 </div>
162 </div>
163 <button onClick={incrementSafe}>
164 Incrément sûr (useCallback)
165 </button>
166 <p className="hint">
167 Utilise une fonction de mise à jour plutôt qu'une référence directe
168 </p>
169 </div>
170 );
171}
172
173// Optimisation avec useRef au lieu du state
174function UseRefOptimizationDemo() {
175 const [, forceRender] = useState(0);
176 const clickCountRef = useRef(0);
177 const renderCountRef = useRef(0);
178
179 renderCountRef.current++;
180
181 const handleClick = () => {
182 clickCountRef.current++;
183 // Ne provoque pas de re-rendu !
184 };
185
186 return (
187 <div className="demo-card">
188 <h2>useRef vs useState</h2>
189 <div className="comparison">
190 <div className="box">
191 <h3>useRef (sans re-rendu)</h3>
192 <p>Clics : {clickCountRef.current}</p>
193 <button onClick={handleClick}>Cliquer (useRef)</button>
194 </div>
195 <div className="box">
196 <h3>Rendus du composant</h3>
197 <p>Nombre : {renderCountRef.current}</p>
198 <button onClick={() => forceRender(n => n + 1)}>
199 Forcer le rendu
200 </button>
201 </div>
202 </div>
203 <p className="hint">
204 useRef ne provoque pas de re-rendu lors du changement de valeur
205 </p>
206 </div>
207 );
208}
209
210function App() {
211 return (
212 <div className="app">
213 <h1>Optimisation de useEffect</h1>
214 <div className="demos">
215 <DependencyArrayDemo />
216 <OptimizedFetchDemo />
217 <InfiniteLoopPreventionDemo />
218 <UseRefOptimizationDemo />
219 </div>
220 </div>
221 );
222}
223
224export 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. Que doit retourner la fonction de nettoyage dans useEffect ?
2. Pourquoi placer un objet directement dans le tableau de dépendances de useEffect peut-il provoquer une boucle infinie ?
Voici 2 des 5 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri vertical
Ordonne dans l'ordre les étapes de rendu de React avec useEffect :
- Éditeur de code
Hook de gestion des frames d'animation
- Éditeur de code
Implémentation d'une recherche débouncée
- Éditeur de code
Crée un composant TemperatureMonitor utilisant useEffect pour simuler des relevés de température toutes les 2 secondes.
- Tri vertical
Ordonne l'évolution de l'optimisation de la gestion des événements :
- Éditeur de code
Crée un composant CommunicationChannel avec useEffect qui établit une connexion et nettoie au démontage.
- Clique dans l'ordre
Ordonne le cycle de vie de useEffect :
- Éditeur de code
Crée un composant EarthCommunication utilisant useEffect pour récupérer des données depuis une API et gérer les états de chargement/erreur.
- Tri vertical
Ordonne le parcours de maîtrise de useEffect :
- Éditeur de code
Crée ColonySettings avec useEffect pour synchroniser l'état avec localStorage (thème, langue, etc.).
- Éditeur de code
Crée ShipDashboard avec useState pour carburant/température/moteur, useEffect avec un intervalle de 2s, et des boutons pour contrôler les systèmes