useEffect
useEffect
est un Hook React qui vous permet de synchroniser un composant React avec un système extérieur.
useEffect(setup, dependencies?)
- Référence
- Utilisation
- Se connecter à un système extérieur
- Enrober vos Effets dans des Hooks personnalisés
- Contrôler un widget non géré par React
- Charger des données avec les Effets
- Spécifier les dépendances réactives
- Mettre à jour l’état sur base d’un état précédent, au sein d’un Effet
- Supprimer des dépendances objets superflues
- Supprimer des dépendances fonctions superflues
- Lire les dernières props et états à jour depuis un Effet
- Afficher un contenu différent côté serveur et côté client
- Dépannage
Référence
useEffect(setup, dependencies?)
Appelez useEffect
à la racine de votre composant pour déclarer un Effet :
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
Voir d’autres exemples ci-dessous.
Paramètres
-
setup
: la fonction contenant la logique de votre Effet. Votre fonction de mise en place peut par ailleurs renvoyer une fonction de nettoyage. Quand votre composant sera ajouté au DOM, React exécutera votre fonction de mise en place. Après chaque nouveau rendu dont les dépendances ont changé, React commencera par exécuter votre fonction de nettoyage (si vous en avez fourni une) avec les anciennes valeurs, puis exécutera votre fonction de mise en place avec les nouvelles valeurs. Une fois votre composant retiré du DOM, React exécutera votre fonction de nettoyage une dernière fois. -
dependencies
optionnelles : la liste des valeurs réactives référencées par le code desetup
. Les valeurs réactives comprennent les props, les variables d’état et toutes les variables et fonctions déclarées localement dans le corps de votre composant. Si votre linter est configuré pour React, il vérifiera que chaque valeur réactive concernée est bien spécifiée comme dépendance. La liste des dépendances doit avoir un nombre constant d’éléments et utiliser un littéral défini à la volée, du genre[dep1, dep2, dep3]
. React comparera chaque dépendance à sa valeur précédente au moyen de la comparaisonObject.is
. Si vous omettez cet argument, votre Effet sera re-exécuté après chaque rendu du composant. Découvrez la différence entre passer un tableau de dépendances, un tableau vide ou aucun tableau.
Valeur renvoyée
useEffect
renvoie undefined
.
Limitations
-
useEffect
est un Hook, vous pouvez donc uniquement l’appeler à la racine de votre composant ou de vos propres Hooks. Vous ne pouvez pas l’appeler à l’intérieur de boucles ou de conditions. Si nécessaire, extrayez un nouveau composant et déplacez l’Effet dans celui-ci. -
Si vous ne cherchez pas à synchroniser avec un système extérieur, c’est que vous n’avez probablement pas besoin d’un Effet.
-
Quand le Mode Strict est activé, React appellera une fois de plus votre cycle mise en place + nettoyage, uniquement en développement, avant la première mise en place réelle. C’est une mise à l’épreuve pour vérifier que votre logique de nettoyage reflète bien votre logique de mise en place, et décommissionne ou défait toute la mise en place effectuée. Si ça entraîne des problèmes, écrivez une fonction de nettoyage.
-
Si certaines de vos dépendances sont des objets ou fonctions définies au sein de votre composant, il existe un risque qu’elles entraînent des exécutions superflues de votre Effet. Pour corriger ça, retirez les dépendances superflues sur des objets et fonctions. Vous pouvez aussi extraire les mises à jour d’état et la logique non réactive hors de votre Effet.
-
Si votre Effet ne découlait pas d’une interaction (telle qu’un clic), React laissera généralement le navigateur rafraîchir l’affichage à l’écran avant d’exécuter votre Effet. Si votre Effet a des aspects visuels (par exemple, il positionne une infobulle) et que le retard est perceptible (par exemple, l’affichage vacille), remplacez
useEffect
paruseLayoutEffect
. -
Même si votre Effet est déclenché par une interaction (telle qu’un clic), le navigateur est susceptible de rafraîchir l’affichage avant d’avoir traité les mises à jour d’état au sein de votre Effet. C’est généralement ce que vous souhaitez. Cependant, si vous devez empêcher le navigateur de rafraîchir l’affichage tout de suite, remplacez
useEffect
paruseLayoutEffect
. -
Les Effets ne sont exécutés que côté client. Ils sont ignorés lors du rendu côté serveur.
Utilisation
Se connecter à un système extérieur
Certains composants ont besoin de rester connectés au réseau, ou à des API du navigateur, ou à des bibliothèques tierces, tout le temps qu’ils sont à l’écran. Ces systèmes ne sont pas gérés par React, on les qualifie donc de systèmes extérieurs.
Afin de connecter votre composant à un système extérieur, appelez useEffect
au niveau racine de votre fonction composant :
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
Vous devez passer deux arguments à useEffect
:
- Une fonction de mise en place avec du code de mise en place qui vous connecte au système.
- Elle devrait renvoyer une fonction de nettoyage avec du code de nettoyage qui vous déconnecte du système.
- Une liste de dépendances comprenant chaque valeur issue de votre composant que ces fonctions utilisent.
React appellera vos fonctions de mise en place et de nettoyage chaque fois que nécessaire, ce qui peut survenir plusieurs fois :
- Votre code de mise en place est exécuté quand votre composant est ajouté à la page (montage).
- Après chaque nouveau rendu de votre composant, si les dépendances ont changé :
- D’abord, votre code de nettoyage est exécuté avec les anciennes props et valeurs d’états.
- Ensuite, votre code de mise en place est exécuté avec les nouvelles props et valeurs d’états.
- Votre code de nettoyage est exécuté une dernière fois lorsque votre composant est retiré de l’arborescence de la page (démontage).
Illustrons cette séquence pour l’exemple précédent.
Lorsque le composant ChatRoom
ci-dessus sera ajouté à la page, il se connectera au salon de discussion en utilisant les valeurs initiales de serverUrl
et roomId
. Si l’une ou l’autre de ces deux valeurs change suite à un nouveau rendu (peut-être l’utilisateur a-t-il choisi un autre salon dans la liste déroulante), votre Effet se déconnectera du salon précédent, puis se connectera au nouveau salon. Lorsque le composant ChatRoom
sera retiré de la page, votre Effet se déconnectera une dernière fois.
Pour vous aider à repérer des bugs, en développement React exécutera un premier cycle de mise en place et de nettoyage, avant d’exécuter la mise en place nominale. C’est une mise à l’épreuve pour vérifier que la logique de votre Effet est implémentée correctement. Si ça entraîne des problèmes, c’est que votre code de nettoyage est manquant ou incomplet. La fonction de nettoyage devrait arrêter ou défaire ce que la fonction de mise en place a initié. La règle à suivre est simple : l’utilisateur ne devrait pas pouvoir faire la différence entre une exécution unique de la mise en place (comme en production) et une séquence mise en place → nettoyage → mise en place (comme en développement). Explorez les solutions courantes.
Essayez d’écrire chaque Effet comme un processus autonome et de réfléchir à un seul cycle de mise en place / nettoyage à la fois. Le fait que votre composant soit en train d’être monté, de se mettre à jour ou d’être démonté ne devrait avoir aucune importance. Lorsque votre logique de nettoyage reflète correctement celle de mise en place, votre Effet n’a aucun problème avec des exécutions multiples de ses codes de mise en place et de nettoyage.
Exemple 1 sur 5 · Se connecter à un serveur de discussion
Dans cet exemple, le composant ChatRoom
utilise un Effet pour rester connecté à un système extérieur défini dans chat.js
. Appuyez sur « Ouvrir le salon » pour que le composant ChatRoom
apparaisse. Ce bac à sable est en mode développement, il y aura donc un cycle supplémentaire de connexion-déconnexion, comme expliqué ici. Essayez de changer roomId
et serverUrl
en utilisant la liste déroulante et le champ de saisie, et voyez comme l’Effet se reconnecte au salon. Appuyez sur « Fermer le salon » pour vous déconnecter une dernière fois.
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => { connection.disconnect(); }; }, [roomId, serverUrl]); return ( <> <label> URL du serveur :{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Bienvenue dans le salon {roomId} !</h1> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); const [show, setShow] = useState(false); return ( <> <label> Choisissez le salon de discussion :{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">Général</option> <option value="travel">Voyage</option> <option value="music">Musique</option> </select> </label> <button onClick={() => setShow(!show)}> {show ? 'Fermer le salon' : 'Ouvrir le salon'} </button> {show && <hr />} {show && <ChatRoom roomId={roomId} />} </> ); }
Enrober vos Effets dans des Hooks personnalisés
Les Effets sont une « échappatoire » : vous vous en servez pour « sortir de React », et lorsqu’il n’y a pas de meilleure solution disponible pour votre cas de figure. Si vous vous retrouvez à souvent écrire manuellement des Effets, c’est généralement le signe que vous devriez en extraire certains sous forme de Hooks personnalisés pour les comportements courants dont vous équipez vos composants.
Par exemple, ce Hook personnalisé useChatRoom
« masque » toute la logique de votre Effet derrière une API plus déclarative.
function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
Vous pouvez dès lors l’utiliser dans n’importe quel composant, comme ceci :
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...
L’écosystème React propose de nombreux excellents Hooks personnalisés pour tous les besoins.
Exemple 1 sur 3 · Hook useChatRoom
personnalisé
Cet exemple est identique à un des exemples précédents, mais sa logique est extraite dans un Hook personnalisé.
import { useState } from 'react'; import { useChatRoom } from './useChatRoom.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); useChatRoom({ roomId: roomId, serverUrl: serverUrl }); return ( <> <label> URL du serveur :{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Bienvenue dans le salon {roomId} !</h1> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); const [show, setShow] = useState(false); return ( <> <label> Choisissez le salon de discussion :{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">Général</option> <option value="travel">Voyage</option> <option value="music">Musique</option> </select> </label> <button onClick={() => setShow(!show)}> {show ? 'Fermer le salon' : 'Ouvrir le salon'} </button> {show && <hr />} {show && <ChatRoom roomId={roomId} />} </> ); }
Contrôler un widget non géré par React
Il peut arriver que vous souhaitiez garder un système extérieur synchronisé avec la valeur d’une prop ou d’un état de votre composant.
Imaginons par exemple que vous ayez un widget tiers de cartographie, ou un composant de lecture vidéo écrit sans React ; vous pouvez utiliser un Effet pour en appeler les méthodes afin que son état soit raccord avec l’état local de votre composant React. L’Effet ci-dessous crée une instance de la classe MapWidget
définie dans map-widget.js
. Lorsque vous modifiez la prop zoomLevel
du composant Map
, l’Effet appelle la méthode setZoom()
sur l’instance pour la garder synchronisée !
import { useRef, useEffect } from 'react'; import { MapWidget } from './map-widget.js'; export default function Map({ zoomLevel }) { const containerRef = useRef(null); const mapRef = useRef(null); useEffect(() => { if (mapRef.current === null) { mapRef.current = new MapWidget(containerRef.current); } const map = mapRef.current; map.setZoom(zoomLevel); }, [zoomLevel]); return ( <div style={{ width: 200, height: 200 }} ref={containerRef} /> ); }
Dans cet exemple, nous n’avons pas besoin d’une fonction de nettoyage parce que la classe MapWidget
ne gère que le nœud DOM qui lui a été passé. Après que le composant React Map
aura été retiré de l’arborescence, tant le nœud DOM que l’instance de MapWidget
seront automatiquement nettoyés par le garbage collector du moteur JavaScript du navigateur.
Charger des données avec les Effets
Vous pouvez utiliser un Effet pour charger des données pour votre composant. Remarquez que si vous utilisez un framework, il sera nettement préférable d’utiliser les mécanismes de chargement de données de votre framework plutôt que le faire manuellement dans des Effets, notamment pour des questions de performances.
Si vous souhaitez charger des données manuellement depuis votre Effet, votre code ressemblera à ceci :
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';
export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);
useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
};
}, [person]);
// ...
Remarquez la variable ignore
, qui est initialisée à false
mais mise à true
lors du nettoyage. Ça garantit que votre code ne souffrira pas de “race conditions” : les réponses réseau pourraient arriver dans un ordre différent de celui de vos envois de requêtes.
import { useState, useEffect } from 'react'; import { fetchBio } from './api.js'; export default function Page() { const [person, setPerson] = useState('Alice'); const [bio, setBio] = useState(null); useEffect(() => { let ignore = false; setBio(null); fetchBio(person).then(result => { if (!ignore) { setBio(result); } }); return () => { ignore = true; } }, [person]); return ( <> <select value={person} onChange={e => { setPerson(e.target.value); }}> <option value="Alice">Alice</option> <option value="Bob">Bob</option> <option value="Clara">Clara</option> </select> <hr /> <p><i>{bio ?? 'Chargement...'}</i></p> </> ); }
Vous pouvez aussi le réécrire en utilisant la syntaxe async
/ await
, mais il vous faudra quand même une fonction de nettoyage :
import { useState, useEffect } from 'react'; import { fetchBio } from './api.js'; export default function Page() { const [person, setPerson] = useState('Alice'); const [bio, setBio] = useState(null); useEffect(() => { async function startFetching() { setBio(null); const result = await fetchBio(person); if (!ignore) { setBio(result); } } let ignore = false; startFetching(); return () => { ignore = true; } }, [person]); return ( <> <select value={person} onChange={e => { setPerson(e.target.value); }}> <option value="Alice">Alice</option> <option value="Bob">Bob</option> <option value="Clara">Clara</option> </select> <hr /> <p><i>{bio ?? 'Chargement...'}</i></p> </> ); }
Implémenter le chargement de données directement dans les Effets devient vite répétitif et complexifie l’ajout ultérieur d’optimisations telles que la mise en cache ou le rendu côté serveur. Il est plus facile d’utiliser un Hook personnalisé — qu’il soit de vous ou maintenu par la communauté.
En détail
Écrire nos appels fetch
dans les Effets constitue une façon populaire de charger des données, en particulier pour des applications entièrement côté client. Il s’agit toutefois d’une approche de bas niveau qui comporte plusieurs inconvénients significatifs :
- Les Effets ne fonctionnent pas côté serveur. Ça implique que le HTML rendu côté serveur avec React proposera un état initial sans données chargées. Le poste client devra télécharger tout le JavaScript et afficher l’appli pour découvrir seulement alors qu’il lui faut aussi charger des données. Ce n’est pas très efficace.
- Charger depuis les Effets entraîne souvent des « cascades réseau ». On affiche le composant parent, il charge ses données, affiche ses composants enfants, qui commencent seulement alors à charger leurs propres données. Si le réseau n’est pas ultra-rapide, cette séquence est nettement plus lente que le chargement parallèle de toutes les données concernées.
- Charger depuis les Effets implique généralement l’absence de pré-chargement ou de cache des données. Par exemple, si le composant est démonté puis remonté, il lui faudrait charger à nouveau les données dont il a besoin.
- L’ergonomie n’est pas top. Écrire ce genre d’appels
fetch
manuels nécessite pas mal de code générique, surtout lorsqu’on veut éviter des bugs tels que les race conditions.
Cette liste d’inconvénients n’est d’ailleurs pas spécifique à React. Elle s’applique au chargement de données lors du montage quelle que soit la bibliothèque. Comme pour le routage, bien orchestrer son chargement de données est un exercice délicat, c’est pourquoi nous vous recommandons plutôt les approches suivantes :
- Si vous utilisez un framework, utilisez son mécanisme intégré de chargement de données. Les frameworks React modernes ont intégré le chargement de données de façon efficace afin d’éviter ce type d’ornières.
- Dans le cas contraire, envisagez l’utilisation ou la construction d’un cache côté client. Les solutions open-source les plus populaires incluent React Query, useSWR, et React Router 6.4+. Vous pouvez aussi construire votre propre solution, auquel cas vous utiliseriez sans doute les Effets sous le capot, mais ajouteriez la logique nécessaire au dédoublonnement de requêtes, à la mise en cache des réponses, et à l’optimisation des cascades réseau (en préchargeant les données ou en consolidant vers le haut les besoins de données des routes).
Vous pouvez continuer à charger les données directement dans les Effets si aucune de ces approches ne vous convient.
Spécifier les dépendances réactives
Remarquez bien que vous ne pouvez pas « choisir » les dépendances de votre Effet. Chaque valeur réactive utilisée par le code de votre Effet doit être déclarée dans votre liste de dépendances, laquelle découle donc du code environnant :
function ChatRoom({ roomId }) { // C’est une valeur réactive
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // Ça aussi
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Cet Effet lit ces valeurs réactives
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ Vous devez donc les lister comme dépendances de votre Effet
// ...
}
Si serverUrl
ou roomId
change, votre Effet se reconnectera à la discussion en utilisant leurs valeurs à jour.
Les valeurs réactives comprennent les props et toutes les variables et fonctions déclarées directement au sein de votre composant. Dans la mesure où roomId
et serverUrl
sont des valeurs réactives, vous ne pouvez pas les retirer de la liste des dépendances. Si vous tentiez de les retirer et que votre linter est correctement configuré pour React, il vous l’interdirait :
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect has missing dependencies: 'roomId' and 'serverUrl'
// ...
}
Pour retirer une dépendance, « prouvez » au linter qu’elle n’a pas besoin d’être une dépendance. Par exemple, vous pouvez déplacer serverUrl
hors de votre composant pour lui prouver qu’elle n’est pas réactive et ne changera pas d’un rendu à l’autre :
const serverUrl = 'https://localhost:1234'; // Ce n’est plus une valeur réactive
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Toutes les dépendances sont déclarées
// ...
}
À présent que serverUrl
n’est plus une valeur réactive (et ne peut plus changer d’un rendu à l’autre), elle n’a plus besoin d’être déclarée comme dépendance. Si le code de votre Effet n’utilise aucune valeur réactive, sa liste de dépendances devrait être vide ([]
) :
const serverUrl = 'https://localhost:1234'; // Ce n’est plus une valeur réactive
const roomId = 'music'; // Ce n’est plus une valeur réactive
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ Toutes les dépendances sont déclarées
// ...
}
Un Effet avec des dépendances vides n’est pas re-exécuté lorsque les valeurs des props ou de l’état de votre composant changent.
Exemple 1 sur 3 · Passer un tableau de dépendances
Si vous spécifiez des dépendances, votre Effet est exécuté après le rendu initial et après les nouveaux rendus qui modifient ces dépendances.
useEffect(() => {
// ...
}, [a, b]); // Re-exécuté si a ou b ont changé
Dans l’exemple ci-dessous, serverUrl
et roomId
sont des valeurs réactives, qui doivent donc toutes les deux être listées comme dépendances. Du coup, sélectionner un autre salon dans la liste déroulante ou modifier l’URL du serveur dans le champ de saisie entraînera une reconnexion de la discussion. En revanche, puisque message
n’est pas utilisé par l’Effet (et n’est donc pas une dépendance), modifier le message n’entraîne pas de reconnexion.
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); const [message, setMessage] = useState(''); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => { connection.disconnect(); }; }, [serverUrl, roomId]); return ( <> <label> URL du serveur :{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Bienvenue dans le salon {roomId} !</h1> <label> Votre message :{' '} <input value={message} onChange={e => setMessage(e.target.value)} /> </label> </> ); } export default function App() { const [show, setShow] = useState(false); const [roomId, setRoomId] = useState('general'); return ( <> <label> Choisissez le salon de discussion :{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">Général</option> <option value="travel">Voyage</option> <option value="music">Musique</option> </select> <button onClick={() => setShow(!show)}> {show ? 'Fermer le salon' : 'Ouvrir le salon'} </button> </label> {show && <hr />} {show && <ChatRoom roomId={roomId}/>} </> ); }
Mettre à jour l’état sur base d’un état précédent, au sein d’un Effet
Lorsque vous souhaitez mettre à jour l’état sur base d’un état précédent depuis un Effet, vous risquez de rencontrer un problème :
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // Vous souhaitez incrémenter le compteur à chaque seconde...
}, 1000)
return () => clearInterval(intervalId);
}, [count]); // 🚩 ... mais préciser `count` comme dépendance réinitialise l'intervalle à chaque fois.
// ...
}
Dans la mesure où count
est une valeur réactive, elle doit figurer dans la liste des dépendances. Pourtant, ça force l’Effet à se nettoyer et se remettre en place chaque fois que count
change. C’est loin d’être idéal.
Pour corriger ça, passez une fonction de mise à jour d’état c => c + 1
à setCount
:
import { useState, useEffect } from 'react'; export default function Counter() { const [count, setCount] = useState(0); useEffect(() => { const intervalId = setInterval(() => { setCount(c => c + 1); // ✅ Passe une fonction de mise à jour }, 1000); return () => clearInterval(intervalId); }, []); // ✅ count n’est plus une dépendance return <h1>{count}</h1>; }
Maintenant que vous passez c => c + 1
au lieu de count + 1
, votre Effet n’a plus besoin de dépendre de count
. En conséquence, il n’aura plus besoin de nettoyer et remettre en place l’intervalle chaque fois que count
change.
Supprimer des dépendances objets superflues
Si votre Effet dépend d’un objet ou d’une fonction créée lors du rendu, il s’exécutera sans doute trop souvent. Par exemple, cet Effet se reconnecte à chaque rendu parce que l’objet options
est en réalité un objet différent à chaque rendu :
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = { // 🚩 Cet objet est (re)créé à chaque rendu
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options); // L’Effet l’utilise
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 Les dépendances sont donc différentes à chaque rendu
// ...
Évitez d’utiliser un objet créé lors du rendu comme dépendance. Préférez créer cet objet au sein de l’Effet :
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); useEffect(() => { const options = { serverUrl: serverUrl, roomId: roomId }; const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [roomId]); return ( <> <h1>Bienvenue dans le salon {roomId} !</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choisissez le salon de discussion :{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">Général</option> <option value="travel">Voyage</option> <option value="music">Musique</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
Maintenant que vous créez l’objet options
au sein de l’Effet, l’Effet lui-même ne dépend plus que de la chaîne de caractères roomId
.
Grâce à ce correctif, modifier la saisie ne reconnecte pas la discussion. Contrairement à un objet créé de frais à chaque fois, un texte comme roomId
ne change pas tant qu’on n’en modifie pas la valeur. Apprenez-en davantage sur l’allègement des dépendances.
Supprimer des dépendances fonctions superflues
Si votre Effet dépend d’un objet ou d’une fonction créée lors du rendu, il s’exécutera sans doute trop souvent. Par exemple, cet Effet se reconnecte à chaque rendu parce que la fonction createOptions
est une fonction différente à chaque rendu :
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() { // 🚩 Cette fonction est (re)créée à chaque rendu
return {
serverUrl: serverUrl,
roomId: roomId
};
}
useEffect(() => {
const options = createOptions(); // L’Effet l’utilise
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🚩 Les dépendances sont donc différentes à chaque rendu
// ...
En soi, créer une fonction à chaque rendu n’est pas un problème. Vous n’avez pas besoin d’optimiser ça. Mais si vous l’utilisez comme dépendance d’un Effet, elle forcera votre Effet à être ré-exécuté à chaque rendu.
Évitez d’utiliser une fonction créée lors du rendu comme dépendance. Déclarez-la plutôt au sein de l’Effet :
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); useEffect(() => { function createOptions() { return { serverUrl: serverUrl, roomId: roomId }; } const options = createOptions(); const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [roomId]); return ( <> <h1>Bienvenue dans le salon {roomId} !</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choisissez le salon de discussion :{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">Général</option> <option value="travel">Voyage</option> <option value="music">Musique</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
Maintenant que vous déclarez la fonction createOptions
au sein de l’Effet, l’Effet lui-même ne dépend plus que de la chaîne de caractères roomId
.
Grâce à ce correctif, modifier la saisie ne reconnecte pas la discussion. Contrairement à une fonction créée de frais à chaque fois, un texte comme roomId
ne change pas tant qu’on n’en modifie pas la valeur. Apprenez-en davantage sur l’allègement des dépendances.
Lire les dernières props et états à jour depuis un Effet
Par défaut, lorsque vous lisez une valeur réactive depuis un Effet, vous devez l’ajouter comme dépendance. Ça garantit que votre Effet « réagit » à chaque modification de cette valeur. Pour la plupart des dépendances, c’est bien le comportement que vous souhaitez.
Toutefois, il peut arriver que vous souhaitiez lire les dernières valeurs à jour de props ou d’états depuis un Effet, sans pour autant y « réagir ». Imaginons par exemple que vous souhaitiez afficher en console le nombre d’éléments dans le panier d’achats à chaque visite de la page :
function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ Toutes les dépendances sont déclarées
// ...
}
Et si vous vouliez afficher une visite de page après chaque modification de url
, mais pas lorsque seul shoppingCart
change ? Vous ne pouvez pas exclure shoppingCart
de vos dépendances sans enfreindre les règles de la réactivité. En revanche, vous pouvez exprimer que vous ne souhaitez pas qu’un bout de votre code « réagisse » aux changements, même s’il est appelé depuis un Effet. Déclarez un Événement d’Effet avec le Hook useEffectEvent
, et déplacez le code qui consulte shoppingCart
à l’intérieur :
function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ Toutes les dépendances sont déclarées
// ...
}
Les Événements d’Effets ne sont pas réactifs et doivent toujours être omis des dépendances de votre Effet. C’est ce qui vous permet d’y mettre du code non réactif (qui peut donc lire la dernière valeur en date de props ou d’états). En lisant shoppingCart
au sein de onVisit
, vous garantissez que shoppingCart
ne redéclenchera pas votre Effet.
Afficher un contenu différent côté serveur et côté client
Si votre appli utilise du rendu côté serveur (que ce soit en direct ou via un framework), votre composant fera son rendu dans deux environnements différents. Côté serveur, son rendu produira le HTML initial. Côté client, React exécutera à nouveau le code de rendu pour pouvoir inscrire les gestionnaires d’événements à ce HTML. C’est pourquoi, afin que l’hydratation puisse fonctionner, votre résultat de rendu initial doit être identique côté client et côté serveur.
Dans de rares cas, vous pourriez avoir besoin de produire des contenus distincts côté client. Disons par exemple que votre appli lit certaines données depuis localStorage
, il ne peut clairement pas faire ça côté serveur. Voici comment vous implémenteriez ça :
function MyComponent() {
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
}, []);
if (didMount) {
// ... renvoi du JSX pour le client seulement ...
} else {
// ... renvoi du JSX initial ...
}
}
Pendant que l’appli charge, l’utilisateur voit le résultat du rendu initial. Puis, lorsqu’elle sera chargée et hydratée, votre Effet sera exécuté et définira didMount
à true
, ce qui déclenchera un nouveau rendu. On basculera alors sur le résultat de rendu pour le client seulement. Les Effets ne sont pas exécutés côté serveur, c’est pourquoi didMount
resterait à false
lors du rendu initial.
N’abusez pas de cette astuce. Gardez à l’esprit que les utilisateurs avec des connexions lentes verront le contenu initial pendant un bon bout de temps — jusqu’à plusieurs secondes — et qu’il faudrait donc éviter d’appliquer au final des changements trop drastiques dans l’apparence de votre composant. Le plus souvent, vous pourrez éviter de recourir à cette approche en utilisant des affichages conditionnels via CSS.
Dépannage
Mon Effet est exécuté deux fois au montage du composant
Lorsque le Mode Strict est activé, en développement, React exécutera une première fois la mise en place et le nettoyage, avant la mise en place effective.
C’est une mise à l’épreuve pour vérifier que la logique de votre Effet est implémentée correctement. Si ça entraîne des problèmes, c’est que votre code de nettoyage est manquant ou incomplet. La fonction de nettoyage devrait arrêter ou défaire ce que la fonction de mise en place a initié. La règle à suivre est simple : l’utilisateur ne devrait pas pouvoir faire la différence entre une exécution unique de la mise en place (comme en production) et une séquence mise en place → nettoyage → mise en place (comme en développement).
Découvrez en quoi ça vous aide à repérer des bugs et comment corriger votre code.
Mon Effet est exécuté après chaque rendu
Commencez par vérifier que vous n’avez pas oublié de spécifier le tableau des dépendances :
useEffect(() => {
// ...
}); // 🚩 Aucun tableau de dépendance : exécuté après chaque rendu !
Si vous avez spécifié un tableau de dépendances et que votre Effet persiste à s’exécuter en boucle, c’est parce qu’une de vos dépendances est différente à chaque rendu.
Vous pouvez déboguer ce problème en affichant vos dépendances en console :
useEffect(() => {
// ..
}, [serverUrl, roomId]);
console.log([serverUrl, roomId]);
Vous pouvez alors cliquer bouton droit, dans la console, sur les tableaux issus de différents rendus et sélectionner « Stocker objet en tant que variable globale » pour chacun d’entre eux. En supposant que vous avez stocké le premier en tant que temp1
et le second en tant que temp2
, vous pouvez alors utiliser la console du navigateur pour vérifier si chaque dépendance des tableaux est identique :
Object.is(temp1[0], temp2[0]); // La première dépendance est-elle inchangée ?
Object.is(temp1[1], temp2[1]); // La deuxième dépendance est-elle inchangée ?
Object.is(temp1[2], temp2[2]); // ... et ainsi de suite pour chaque dépendance ...
Lorsque vous aurez repéré la dépendance qui diffère d’un rendu à l’autre, vous pouvez généralement corriger ça de l’une des manières suivantes :
- Mettre à jour l’état sur base d’un état précédent, au sein d’un Effet
- Supprimer des dépendances objets superflues
- Supprimer des dépendances fonctions superflues
- Lire les dernières props et états à jour depuis un Effet
En tout dernier recours (si aucune de ces approches n’a résolu le souci), enrobez la création de la dépendance avec useMemo
(ou useCallback
pour les fonctions).
Mon Effet n’arrête pas de se re-exécuter
Si votre Effet s’exécute en boucle infinie, deux choses devraient se passer :
- Votre Effet met à jour un état.
- Cet état entraîne un nouveau rendu, qui modifie les dépendances de votre Effet.
Avant de vous attaquer à la résolution de ce problème, demandez-vous si votre Effet se connecte à un système extérieur (tel que le DOM, le réseau, un widget tiers, etc.). Pourquoi votre Effet a-t-il besoin de modifier l’état ? Se synchronise-t-il avec un système extérieur ? Ou essayez-vous de gérer le flux de données de votre application avec ça ?
S’il n’y a pas de système extérieur, envisagez de retirer carrément l’Effet pour simplifier votre logique.
Si vous vous synchronisez effectivement avec un système extérieur, réfléchissez aux conditions dans lesquelles votre Effet devrait mettre à jour l’état. Quelque chose a-t-il changé qui impacte le résultat visuel de votre composant ? Si vous devez surveiller certaines données inutilisées par le rendu, une ref (qui ne redéclenche pas de rendu) serait peut-être plus appropriée. Vérifiez que votre Effet ne met pas à jour l’état (entraînant un nouveau rendu) plus que nécessaire.
Pour finir, si votre Effet met à jour l’état au bon moment, mais qu’il y a tout de même une boucle, c’est sans doute parce que la mise à jour de l’état entraîne la modification d’une des dépendances de l’Effet. Voyez comment déboguer les modifications de dépendances.
Ma logique de nettoyage est exécutée alors que mon composant n’a pas été démonté
La fonction de nettoyage n’est pas exécutée seulement lors du démontage, mais avant chaque nouveau rendu dont les dépendances ont changé. Qui plus est, en développement, React exécute la mise en place et le nettoyage une fois de plus juste après le montage du composant.
Si vous avez du code de nettoyage qui ne correspond à aucun code de mise en place, c’est généralement mauvaise signe :
useEffect(() => {
// 🔴 À éviter : code de nettoyage sans mise en place correspondante
return () => {
doSomething();
};
}, []);
Votre code de nettoyage devrait refléter celui de mise en place, qu’il devrait arrêter ou défaire :
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
Apprenez en quoi le cycle de vie des Effets diffère de celui des composants.
Mon Effet fait un truc visuel, et l’affichage vacille avant son exécution
Si votre Effet doit empêcher le navigateur de rafraîchir immédiatement l’affichage à l’écran, remplacez useEffect
par useLayoutEffect
. Remarquez que ça ne devrait concerner qu’une toute petite minorité de cas. Vous n’aurez besoin de ça que lorsqu’il est crucial que votre Effet soit exécuté avant le rafraîchissement par le navigateur ; par exemple, pour mesurer et positionner une infobulle avant que l’utilisateur ne la voie.