Un composant de chat reconnecte son WebSocket chaque fois que vous tapez une lettre dans un champ de recherche qui n’a rien à voir avec le chat. Un autre composant se re-render en boucle jusqu’à ce que l’onglet se bloque. Un troisième affiche son useEffect deux fois au montage, mais seulement en développement. Aucun de ces cas n’est un bug de React. Les deux premiers sont le tableau de dépendances qui fait exactement ce que vous lui avez demandé, et le troisième est Strict Mode qui vérifie que votre effet sait se nettoyer après lui-même.
Quand utiliser useEffect dans un composant React
useEffect synchronise un composant avec quelque chose qui vit en dehors de React : une connexion serveur, une API du navigateur, un timer, le DOM directement, un widget tiers. React rend votre JSX, l’applique au DOM, puis exécute vos effets. Quand le rendu n’a pas été déclenché par une interaction comme un clic, React laisse généralement le navigateur peindre le nouvel écran d’abord.
Un effet est donc le mauvais endroit pour calculer une valeur destinée au rendu. Si fullName peut être dérivé de firstName et lastName, écrivez-le pendant le rendu :
| |
Pas un effet qui appelle setFullName après coup. La version avec effet rend une fois avec la valeur périmée, planifie une mise à jour d’état, puis rend à nouveau. Ce passage supplémentaire n’apporte rien. Utilisez useEffect quand le composant doit faire quelque chose parce qu’il a été affiché, pas pour calculer quelque chose qu’il pourrait calculer directement.
Comment fonctionne le tableau de dépendances de useEffect
Le deuxième argument de useEffect se présente sous trois formes, et chacune indique à React quelque chose de différent sur le moment où relancer la fonction.
| |
Pour un tableau rempli, React compare chaque entrée à sa valeur au rendu précédent avec Object.is. C’est la même vérification que ===, sauf que NaN est égal à lui-même et que 0 et -0 sont différents. Si chaque dépendance est identique à la dernière fois, React saute l’effet. Si une seule diffère, il s’exécute à nouveau. Il n’y a pas de correspondance partielle, ni de moyen de dire à React « relance seulement quand cette dépendance précise a changé ». Cette logique doit vivre à l’intérieur de l’effet.
Cela signifie aussi que vous ne pouvez pas omettre une valeur du tableau pour empêcher l’effet de se relancer. Le tableau doit lister chaque valeur réactive que le corps de la fonction lit : état, props, et tout ce qui en dérive. Retirez une valeur du tableau tout en continuant à la lire, et l’effet tourne avec une copie périmée. La règle react-hooks/exhaustive-deps d’eslint-plugin-react-hooks existe précisément pour détecter ce décalage. Traitez-la comme une erreur de build, ne la réduisez pas au silence avec un commentaire de désactivation.
Ce que fait la fonction de cleanup de useEffect
Un effet peut retourner une fonction. React traite cette valeur de retour comme le nettoyage de ce que l’effet a démarré :
| |
React appelle le cleanup dans deux situations : juste avant que l’effet se relance parce qu’une dépendance a changé, et quand le composant est démonté. Il nettoie toujours l’effet précédent avant de démarrer le suivant. Connexion, déconnexion, connexion, déconnexion. Jamais deux connexions ouvertes à la fois.
Tout ce qu’un effet démarre et qui a besoin d’un arrêt explicite doit se trouver derrière une fonction de cleanup : subscribe a besoin d’unsubscribe, addEventListener a besoin de removeEventListener, setInterval a besoin de clearInterval. Le même schéma corrige un bug plus subtil de récupération de données, où une réponse périmée arrive après une plus récente :
| |
Si personId change avant que la première requête se résolve, le cleanup met ignore à true pour cette requête, donc son callback .then() ne fait rien au lieu d’écraser un état plus récent avec des données plus anciennes.
Pourquoi useEffect s’exécute deux fois en développement
En développement, sous StrictMode, React exécute le setup, puis le cleanup, puis le setup à nouveau, immédiatement après le premier montage. En production, le setup s’exécute une seule fois. React le fait intentionnellement : votre composant sera démonté puis remonté en usage réel (quitter une route et y revenir, par exemple), et Strict Mode force cela à se produire d’abord sur votre machine, où un cleanup manquant apparaît comme un bug visible.
L’erreur consiste à corriger le symptôme :
| |
Le garde-fou bloque le second appel à connect(), mais il n’y a toujours pas de cleanup, donc la connexion reste ouverte après le démontage du composant. Si le remontage casse un effet, c’est le cleanup de l’effet qui manque. Écrivez la fonction de cleanup plutôt que de vous défendre contre la seconde exécution.
Erreurs courantes avec les dépendances de useEffect
Un objet ou une fonction littérale recréés à chaque rendu. Deux objets avec un contenu identique ne sont pas le même objet :
| |
Placez un objet comme celui-ci dans un tableau de dépendances et l’effet voit une valeur « nouvelle » à chaque rendu, que quelque chose d’important ait changé ou non :
| |
Construisez l’objet à l’intérieur de l’effet à la place, et dépendez des valeurs primitives dont il est fait :
| |
Lire l’état seulement pour calculer l’état suivant. Un interval qui lit count pour l’incrémenter force l’effet à dépendre de count, ce qui recrée l’interval à chaque tick :
| |
Une mise à jour fonctionnelle supprime cette lecture, et la dépendance part avec elle :
| |
Un effet qui fait deux tâches sans rapport. Imaginez un effet qui récupère les villes d’un pays et les quartiers d’une ville. Avec les deux dans la même fonction et le même tableau de dépendances, changer de ville relance aussi la récupération des villes. Séparez-les : un effet par élément indépendant de synchronisation, chacun avec son propre tableau.
Quand vous n’avez pas besoin de useEffect
Deux schémas semblent nécessiter un effet et n’en ont pas besoin.
Réinitialiser l’état quand une prop change. Vider l’état local d’un formulaire quand userId change est tentant à écrire comme un effet qui observe userId. Ça fonctionne, mais le composant rend une fois avec l’ancien état avant que la réinitialisation arrive. Passez plutôt la valeur qui change comme key du composant, et React la traite comme une instance neuve, sans aucun effet impliqué :
| |
Quelque chose qui se produit à cause d’un clic, pas d’un rendu. Une notification « ajouté au panier » qui observe product.isInCart dans un effet se déclenche à nouveau à chaque rechargement de page où l’article est déjà dans le panier, car l’effet ne peut pas distinguer « vient d’être ajouté » de « déjà présent ». La notification appartient au handler du clic qui ajoute l’article, où elle ne s’exécute que lorsque l’utilisateur clique :
| |
Si quelque chose doit arriver parce que l’utilisateur a fait quelque chose, placez-le dans le gestionnaire d’événement de ce quelque chose. useEffect est fait pour ce qui doit rester synchronisé tout le temps que le composant est à l’écran, indépendamment de ce que fait l’utilisateur. Synchroniser une requête avec le cycle de vie du composant est ce genre de problème (le côté requête est traité dans la gestion des erreurs de la Fetch API), tout comme déconnecter un observer au démontage, comme avec l’Intersection Observer API.
La prochaine fois qu’un effet se comporte mal, laissez le tableau tranquille au début. Listez ce que le corps de la fonction lit, décidez si chaque lecture doit vraiment être réactive, et vérifiez si le code fonctionne sans aucun effet. Corrigez le corps, et le tableau correct en découle. Si c’est l’ordre des callbacks asynchrones autour d’un rendu qui vous perturbe, lisez comment fonctionnent async/await et l’event loop, qui couvre la call stack et la file des microtasks.