Ce Hook mérite-t-il sa place ? Dix questions de révision pour le code React
Apprenez dix questions de révision concrètes permettant de détecter les états redondants, les effets obsolètes, les boucles de rendu, la mémorisation inutile et les composants trop volumineux dans du code React Hook.
Sources: Hooks
La plupart des bugs liés aux React Hook ne ressemblent pas à des bugs dans une demande de fusion. Une dépendance manquante, un effet qui copie des propriétés dans l’état, ou encore un useCallback entourant un gestionnaire que personne ne mémorise sont tous acceptés lors de la revue, avant de se manifester plus tard sous forme d’écrans obsolètes, de rendus supplémentaires ou d’écouteurs accumulés. Ci-dessous, dix erreurs fréquentes liées aux Hook sont transformées en questions de revue que vous pouvez poser à n’importe quel composant, accompagnées du mécanisme sous-jacent à chacune d’elles ainsi que de la forme simplifiée que le code devrait généralement adopter.
Questions sur l’état
Ce valeur peut-elle être calculée pendant le rendu ?
Un schéma courant consiste à conserver fullName dans l’état et à utiliser un effet pour appeler setFullName(`${firstName} ${lastName}`) chaque fois qu’un des noms change. Le composant s’affiche une première fois avec la valeur ancienne, l’effet s’exécute après le rendu, et le setteur planifie un deuxième affichage. Pendant une frame, l’interface utilisateur est incohérente, et il y a un élément supplémentaire à gérer.
Mieux vaut le calculer directement : const fullName = `${firstName} ${lastName}`;. React réexécute le composant à chaque rendu, de sorte que la valeur reste toujours en synchronisation avec ses entrées. Si quelque chose peut être dérivé des props ou de l’état, ce n’est pas de l’état. Voir également pourquoi synchroniser l’état avec useEffect a des effets néfastes.
Le composant conserve-t-il plus d’état qu’il n’en a besoin ?
Même erreur à une échelle plus grande : un formulaire avec trois appels à useState pour firstName, lastName et fullName. Chaque modification de nom doit désormais se refléter dans fullName, et tout chemin qui oublie cela crée une erreur. Conservez les deux champs d’entrée indépendants et déduisez le troisième. Pour chaque useState, demandez-vous si la valeur peut être reconstruite à partir des autres ; dans ce cas, supprimez-la.
Faut-il mettre à jour l’écran en changeant cette valeur ?
L’état a pour but de piloter le rendu. Un identifiant de timer, un nœud DOM, une valeur précédente d’une propriété ou une instance tierce (un tableau, un graphique, un client socket) n’a généralement aucun impact sur ce qui est affiché ; par conséquent, utiliser useState(null) pour eux signifie que chaque affectation déclenche un rendu inutile. Préférez plutôt const timerRef = useRef(null); : l’écriture de timerRef.current conserve sa valeur entre les rendus sans avoir à planifier de nouveau rendu. L’inconvénient est que vous ne devez jamais lire un ref pour déterminer ce qui doit être affiché. Si l’interface utilisateur doit refléter une valeur, il s’agit après tout d’état.
Questions sur les effets
L’array de dépendances liste-t-il tout ce que l’effet lit ?
Lorsque l’on passe [] pour que l’effet s’exécute « une seule fois », comme dans useEffect(() => { fetchUser(userId); }, []);, le userId est figé à partir de la première rendu. Lorsque la propriété change, rien n’est rechargé et l’interface affiche le mauvais utilisateur. Utiliser [userId] résout ce problème.
L’array n’est pas un paramètre de planification ; il indique quels valeurs l’effet lit. Si l’effet s’exécute trop souvent, modifiez ce qu’il lit (placez une fonction à l’intérieur, dépendez d’une valeur primitive plutôt que d’un objet) au lieu de cacher les dépendances. La règle de linting react-hooks/exhaustive-deps détecte presque tous ces cas, il convient donc de traiter ses avertissements comme des erreurs. Les effets de chargement devraient également ignorer ou annuler les requêtes obsolètes lors du nettoyage, lorsque userId change rapidement.
Cet effet peut-il se déclencher lui-même ?
useEffect(() => { setCount(count + 1); }, [count]); est une boucle : la mise à jour modifie count, ce qui fait redémarrer l’effet indéfiniment. L’actualiseur fonctionnel setCount(prev => prev + 1) lit la valeur la plus récente sans avoir besoin de count dans la clôture, ce qui permet d’éliminer cette dépendance et de faire en sorte que l’effet ne s’exécute qu’à la montée du composant. Notez cependant qu’il augmente toujours d’une unité à chaque montée, et que le mode strict fait monter le composant deux fois en environnement de développement, ce qui peut faire apparaître une augmentation de deux unités localement.
La meilleure question est souvent de savoir si la mise à jour doit vraiment faire partie d’un effet. Les réponses aux actions de l’utilisateur sont gérées par des gestionnaires d’événements ; les valeurs dérivées sont calculées lors du rendu. Les effets servent à synchroniser le composant avec des systèmes externes à React.
L’effet annule-t-il ce qu’il a configuré ?
Un effet qui ne fait que l’appel window.addEventListener('resize', handleResize) ajoute un nouvel écouteur à chaque montage et ne le supprime jamais. Il faut retourner une fonction de nettoyage qui appelle window.removeEventListener avec le même gestionnaire. React l’exécute avant que l’effet ne soit exécuté à nouveau et lors du démontage. Sans cela, on rencontre des fuites de mémoire, des gestionnaires qui s’exécutent plusieurs fois et des mises à jour de composants qui ont disparu.
Vérification rapide : chaque add, connect ou setInterval dans un effet nécessite une fonction remove, disconnect ou clearInterval correspondante dans sa valeur de retour. L’audit des dix erreurs fréquentes avec useEffect couvre davantage de modes de défaillance des effets.
Questions sur la mémorisation
Cette calcul est-il vraiment coûteux ?
Envelopper une concaténation de chaînes dans useMemo avec [user] comme dépendance coûte plus que ce qu’il économise. React alloue toujours la fonction de rappel ainsi que le tableau et compare les dépendances à chaque rendu, ce qui représente plus de travail que simplement joindre deux chaînes. Le code devient également plus difficile à lire, et si user est un nouvel objet à chaque rendu, le mémoisation se recalcule de toute façon.
Utilisez plutôt l’expression simple. Recourez à useMemo uniquement pour des opérations particulièrement coûteuses, comme trier ou filtrer une grande liste, ou lorsque un enfant mémoisé ou un autre Hook a besoin d’une référence à un objet stable. Mesurez d’abord les performances avec le Profilleur des outils de développement React.
Quelque chose dépend-il de l’identité de cette fonction ?
useCallback maintient stable la référence d’une fonction, ce qui n’a d’importance que lorsque quelque chose compare des références : un enfant enveloppé dans React.memo ou la liste des dépendances d’un autre Hook. Envelopper un gestionnaire de clic qui se contente de logger, attaché à un button ordinaire, ne sert à rien, car le DOM ne se soucie pas du fait que la fonction soit nouvelle. Écrivez une fonction ordinaire et ajoutez useCallback lorsque vous pouvez identifier l’élément qui en a besoin. Le compilateur React peut appliquer cette mise en mémoire automatiquement ; consultez la documentation actuelle pour connaître son état dans votre configuration.
Questions sur la structure
Tous les Hooks sont-ils appelés inconditionnellement, dans le même ordre ?
Déclarer useState uniquement à l’intérieur de if (isLoggedIn) viole les Règles des Hooks. React associe les Hooks en fonction de leur position dans la liste propre à chaque composant, et non selon leur nom. En sautant une appel, chaque Hook suivant lit la case de son voisin, ce qui produit des valeurs incorrectes ou une erreur en temps de exécution liée à un nombre modifié de Hooks.
Appelez tous les Hooks au niveau le plus élevé, puis créez des branches : déclarez d’abord l’état user et renvoyez null par la suite si l’utilisateur n’est pas connecté. Il en va de même pour les boucles et les retours anticipés. Pour en savoir plus sur le fonctionnement interne, consultez pourquoi les Règles des Hooks existent.
Ce composant effectue-t-il trop de tâches ?
Un composant qui mélange des centaines de lignes de logique de récupération, d’état et métier avec du markup est difficile à lire, à tester et à modifier. Extraitez cette logique dans un Hook personnalisé. Un Hook useUserData(userId) gère l’état user ainsi que l’effet qui appelle fetchUser(userId).then(setUser) lorsque userId change, avant de retourner user. UserProfile se réduit alors à const user = useUserData(userId); suivi de l’affichage de user?.name.
Cela vous offre :
- La possibilité de réutiliser la logique sans la copier entre composants.
- Des tests pour la logique indépendamment du markup.
- Des composants plus courts et faciles à parcourir.
- Un seul endroit pour modifier les opérations de récupération ou de mise en cache.
N’oubliez pas que chaque appelant d’un Hook personnalisé dispose de son propre état : il partage la logique, mais pas les données.
Points clés
- Les valeurs dérivées sont des expressions, ni de l’état ni des effets.
- Gardez l’état au minimum ; les valeurs qui n’atteignent jamais l’écran vont dans des refs.
- Les tableaux de dépendances décrivent ce qu’un effet lit, donc vérifiez-les honnêtement.
- Chaque effet qui configure quelque chose doit le déconfigurer à son tour.
- Mémoisez uniquement lorsque cela est justifié par une mesure ou un consommateur sensible aux refs.
- Appeliez les Hooks inconditionnellement, et déplacez de gros blocs logiques dans des Hooks personnalisés.
La plupart de ces problèmes proviennent du fait de saisir un Hook par habitude. Se demander « avons-nous vraiment besoin de ce Hook ici ? » lors des revues est peu coûteux et permet d’éviter de longues sessions de débogage.