Maîtrisez le hook useCallback de React. Apprenez ce qu'est la mémorisation de fonctions, quand (et quand ne pas) l'utiliser, et comment optimiser vos composants pour la performance.
React useCallback : un approfondissement de la mémorisation des fonctions et de l'optimisation des performances
Dans le monde du développement web moderne, React se distingue par son interface utilisateur déclarative et son modèle de rendu efficace. Cependant, à mesure que les applications gagnent en complexité, garantir des performances optimales devient une responsabilité essentielle pour chaque développeur. React fournit une suite d'outils puissants pour relever ces défis, et parmi les plus importants (et souvent mal compris) figurent les hooks d'optimisation. Aujourd'hui, nous allons approfondir l'un d'eux : useCallback.
Ce guide complet démystifiera le hook useCallback. Nous explorerons le concept JavaScript fondamental qui le rend nécessaire, comprendrons sa syntaxe et ses mécanismes et, surtout, établirons des directives claires sur les moments où vous devriez (et ne devriez pas) l'utiliser dans votre code. À la fin, vous serez équipé pour utiliser useCallback non pas comme une solution miracle, mais comme un outil précis pour rendre vos applications React plus rapides et plus efficaces.
Le problème principal : comprendre l'égalité référentielle
Avant de pouvoir apprécier ce que fait useCallback, nous devons d'abord comprendre un concept essentiel en JavaScript : l'égalité référentielle. En JavaScript, les fonctions sont des objets. Cela signifie que lorsque vous comparez deux fonctions (ou deux objets quelconques), vous ne comparez pas leur contenu mais leur référence, leur emplacement spécifique en mémoire.
Considérez cet simple extrait de code JavaScript :
const func1 = () => { console.log('Hello'); };
const func2 = () => { console.log('Hello'); };
console.log(func1 === func2); // Affiche : false
Même si func1 et func2 ont un code identique, ce sont deux objets de fonction distincts créés à des adresses mémoire différentes. Par conséquent, ils ne sont pas égaux.
Comment cela affecte les composants React
Un composant fonctionnel React est, à la base, une fonction qui s'exécute chaque fois que le composant doit être rendu. Cela se produit lorsque son état change, ou lorsque son composant parent est rendu à nouveau. Lorsque cette fonction s'exécute, tout ce qui se trouve à l'intérieur, y compris les déclarations de variables et de fonctions, est recréé à partir de zéro.
Examinons un composant typique :
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
// Cette fonction est recréée à chaque rendu
const handleIncrement = () => {
console.log('Création d'une nouvelle fonction handleIncrement');
setCount(count + 1);
};
return (
Compteur : {count}
);
};
Chaque fois que vous cliquez sur le bouton « Incrémenter », l'état count change, ce qui entraîne le rendu à nouveau du composant Counter. Lors de chaque nouveau rendu, une toute nouvelle fonction handleIncrement est créée. Pour un composant simple comme celui-ci, l'impact sur les performances est négligeable. Le moteur JavaScript est incroyablement rapide pour créer des fonctions. Alors, pourquoi devons-nous même nous en soucier ?
Pourquoi recréer des fonctions devient un problème
Le problème n'est pas la création de la fonction elle-même ; c'est la réaction en chaîne qu'elle peut provoquer lorsqu'elle est transmise en tant que prop à des composants enfants, en particulier ceux optimisés avec React.memo.
React.memo est un composant d'ordre supérieur (HOC) qui mémorise un composant. Il fonctionne en effectuant une comparaison superficielle des props du composant. Si les nouvelles props sont les mêmes que les anciennes, React ignorera le rendu à nouveau du composant et réutilisera le dernier résultat rendu. Il s'agit d'une optimisation puissante pour empêcher les cycles de rendu inutiles.
Voyons maintenant où intervient notre problème d'égalité référentielle. Imaginez que nous ayons un composant parent qui transmet une fonction de gestion à un composant enfant mémorisé.
import React, { useState } from 'react';
// Un composant enfant mémorisé qui n'est rendu à nouveau que si ses props changent.
const MemoizedButton = React.memo(({ onIncrement }) => {
console.log('MemoizedButton est en cours de rendu !');
return ;
});
const ParentComponent = () => {
const [count, setCount] = useState(0);
const [otherState, setOtherState] = useState(false);
// Cette fonction est recréée chaque fois que ParentComponent est rendu
const handleIncrement = () => {
setCount(count + 1);
};
return (
Compteur parent : {count}
Autre état : {String(otherState)}
);
};
Dans cet exemple, MemoizedButton reçoit une prop : onIncrement. Vous pourriez vous attendre à ce que lorsque vous cliquez sur le bouton « Basculer l'autre état », seul le ParentComponent soit rendu à nouveau car le count n'a pas changé, et donc la fonction onIncrement est logiquement la même. Cependant, si vous exécutez ce code, vous verrez « MemoizedButton est en cours de rendu ! » dans la console chaque fois que vous cliquez sur « Basculer l'autre état ».
Pourquoi cela se produit-il ?
Lorsque ParentComponent est rendu à nouveau (en raison de setOtherState), il crée une nouvelle instance de la fonction handleIncrement. Lorsque React.memo compare les props pour MemoizedButton, il constate que oldProps.onIncrement !== newProps.onIncrement en raison de l'égalité référentielle. La nouvelle fonction se trouve à une adresse mémoire différente. Cette vérification échouée force notre enfant mémorisé à être rendu à nouveau, ce qui annule complètement l'objectif de React.memo.
C'est le principal scénario où useCallback vient à la rescousse.
La solution : mémoriser avec `useCallback`
Le hook useCallback est conçu pour résoudre ce problème précis. Il vous permet de mémoriser une définition de fonction entre les rendus, garantissant qu'elle maintient l'égalité référentielle à moins que ses dépendances ne changent.
Syntaxe
const memoizedCallback = useCallback(
() => {
// La fonction à mémoriser
doSomething(a, b);
},
[a, b], // Le tableau de dépendances
);
- Premier argument : La fonction de rappel en ligne que vous souhaitez mémoriser.
- Deuxième argument : Un tableau de dépendances.
useCallbackne renverra une nouvelle fonction que si l'une des valeurs de ce tableau a changé depuis le dernier rendu.
Refactorisons notre exemple précédent à l'aide de useCallback :
import React, { useState, useCallback } from 'react';
const MemoizedButton = React.memo(({ onIncrement }) => {
console.log('MemoizedButton est en cours de rendu !');
return ;
});
const ParentComponent = () => {
const [count, setCount] = useState(0);
const [otherState, setOtherState] = useState(false);
// Maintenant, cette fonction est mémorisée !
const handleIncrement = useCallback(() => {
setCount(count + 1);
}, [count]); // Dépendance : 'count'
return (
Compteur parent : {count}
Autre état : {String(otherState)}
);
};
Maintenant, lorsque vous cliquez sur « Basculer l'autre état », le ParentComponent est rendu à nouveau. React exécute le hook useCallback. Il compare la valeur de count dans son tableau de dépendances avec la valeur du rendu précédent. Étant donné que count n'a pas changé, useCallback renvoie la même instance de fonction exacte qu'il a renvoyée la dernière fois. Lorsque React.memo compare les props pour MemoizedButton, il constate que oldProps.onIncrement === newProps.onIncrement. La vérification est réussie et le rendu à nouveau inutile de l'enfant est ignoré avec succès ! Problème résolu.
Maîtriser le tableau de dépendances
Le tableau de dépendances est la partie la plus critique de l'utilisation correcte de useCallback. Il indique à React quand il est sûr de recréer la fonction. Se tromper peut entraîner des bugs subtils difficiles à traquer.
Le tableau vide : `[]`
Si vous fournissez un tableau de dépendances vide, vous dites à React : « Cette fonction n'a jamais besoin d'être recréée. La version du rendu initial est bonne pour toujours. »
const stableFunction = useCallback(() => {
console.log('Ce sera toujours la même fonction');
}, []); // Tableau vide
Cela crée une référence très stable, mais cela s'accompagne d'une mise en garde majeure : le problème de la « fermeture obsolète ». Une fermeture se produit lorsqu'une fonction « se souvient » des variables de la portée dans laquelle elle a été créée. Si votre rappel utilise un état ou des props mais que vous ne les répertoriez pas comme dépendances, il se fermera sur leurs valeurs initiales.
Exemple d'une fermeture obsolète :
const StaleCounter = () => {
const [count, setCount] = useState(0);
const handleLogCount = useCallback(() => {
// Ce 'count' est la valeur du rendu initial (0)
// car `count` ne figure pas dans le tableau de dépendances.
console.log(`Le compteur actuel est : ${count}`);
}, []); // FAUX ! Dépendance manquante
return (
Compteur : {count}
);
};
Dans cet exemple, peu importe combien de fois vous cliquez sur « Incrémenter », cliquer sur « Enregistrer le compteur » affichera toujours « Le compteur actuel est : 0 ». La fonction handleLogCount est bloquée avec la valeur de count du premier rendu car son tableau de dépendances est vide.
Le tableau correct : `[dep1, dep2, ...]`
Pour résoudre le problème de la fermeture obsolète, vous devez inclure chaque variable de la portée du composant (état, props, etc.) que votre fonction utilise à l'intérieur du tableau de dépendances.
const handleLogCount = useCallback(() => {
console.log(`Le compteur actuel est : ${count}`);
}, [count]); // CORRECT ! Maintenant, cela dépend du compteur.
Maintenant, chaque fois que count change, useCallback créera une nouvelle fonction handleLogCount qui se fermera sur la nouvelle valeur de count. C'est la façon correcte et sûre d'utiliser le hook.
Conseil de pro : Utilisez toujours le package eslint-plugin-react-hooks. Il fournit une règle `exhaustive-deps` qui vous avertira automatiquement si vous manquez une dépendance dans vos hooks `useCallback`, `useEffect` ou `useMemo`. C'est un filet de sécurité inestimable.
Modèles et techniques avancés
1. Mises à jour fonctionnelles pour éviter les dépendances
Parfois, vous souhaitez une fonction stable qui met à jour l'état, mais vous ne voulez pas la recréer chaque fois que l'état change. Ceci est courant pour les fonctions transmises aux hooks personnalisés ou aux fournisseurs de contexte. Vous pouvez y parvenir en utilisant la forme de mise à jour fonctionnelle d'un setter d'état.
const handleIncrement = useCallback(() => {
// `setCount` peut prendre une fonction qui reçoit l'état précédent.
// De cette façon, nous n'avons pas besoin de dépendre directement de `count`.
setCount(prevCount => prevCount + 1);
}, []); // Le tableau de dépendances peut maintenant être vide !
En utilisant setCount(prevCount => ...), notre fonction n'a plus besoin de lire la variable count de la portée du composant. Parce qu'elle ne dépend de rien, nous pouvons utiliser en toute sécurité un tableau de dépendances vide, créant une fonction qui est vraiment stable pour tout le cycle de vie du composant.
2. Utiliser `useRef` pour les valeurs volatiles
Que se passe-t-il si votre rappel doit accéder à la dernière valeur d'une prop ou d'un état qui change très fréquemment, mais que vous ne voulez pas rendre votre rappel instable ? Vous pouvez utiliser un `useRef` pour conserver une référence mutable à la dernière valeur sans déclencher de nouveaux rendus.
const VeryFrequentUpdates = ({ onEvent }) => {
const [value, setValue] = useState('');
// Conserver une référence à la dernière version du rappel onEvent
const onEventRef = useRef(onEvent);
useEffect(() => {
onEventRef.current = onEvent;
}, [onEvent]);
// Ce rappel interne peut être stable
const handleInternalAction = useCallback(() => {
// ...une certaine logique interne...
// Appeler la dernière version de la fonction prop via la référence
if (onEventRef.current) {
onEventRef.current();
}
}, []); // Fonction stable
// ...
};
Il s'agit d'un modèle avancé, mais il est utile dans des scénarios complexes tels que le défilement, la limitation ou l'interface avec des bibliothèques tierces qui nécessitent des références de rappel stables.
Conseil crucial : quand NE PAS utiliser `useCallback`
Les nouveaux venus dans les hooks React tombent souvent dans le piège de l'enveloppement de chaque fonction dans useCallback. Il s'agit d'un anti-modèle connu sous le nom d'optimisation prématurée. N'oubliez pas que useCallback n'est pas gratuit ; il a un coût de performance.
Le coût de `useCallback`
- Mémoire : Il doit stocker la fonction mémorisée en mémoire.
- Calcul : À chaque rendu, React doit toujours appeler le hook et comparer les éléments du tableau de dépendances avec leurs valeurs précédentes.
Dans de nombreux cas, ce coût peut l'emporter sur l'avantage. La surcharge de l'appel du hook et de la comparaison des dépendances peut être supérieure au coût de la simple recréation de la fonction et du rendu à nouveau d'un composant enfant.
N'utilisez PAS `useCallback` lorsque :
- La fonction est transmise à un élément HTML natif : Les composants tels que
<div>,<button>ou<input>ne se soucient pas de l'égalité référentielle pour leurs gestionnaires d'événements. La transmission d'une nouvelle fonction àonClickà chaque rendu est parfaitement acceptable et n'a aucun impact sur les performances. - Le composant de réception n'est pas mémorisé : Si vous transmettez un rappel à un composant enfant qui n'est pas enveloppé dans
React.memo, la mémorisation du rappel n'a aucun intérêt. Le composant enfant sera de toute façon rendu à nouveau chaque fois que son parent est rendu à nouveau. - La fonction est définie et utilisée dans le cycle de rendu d'un seul composant : Si une fonction n'est pas transmise en tant que prop ou utilisée comme dépendance dans un autre hook, il n'y a aucune raison de mémoriser sa référence.
// Pas besoin de useCallback ici
const handleClick = () => { console.log('Cliqué !'); };
return ;
La règle d'or : N'utilisez useCallback que comme une optimisation ciblée. Utilisez le profileur des outils de développement React pour identifier les composants qui sont rendus à nouveau inutilement. Si vous trouvez un composant enveloppé dans React.memo qui est toujours rendu à nouveau en raison d'une prop de rappel instable, c'est le moment idéal pour appliquer useCallback.
`useCallback` vs. `useMemo` : la principale différence
Un autre point de confusion courant est la différence entre useCallback et useMemo. Ils sont très similaires, mais servent à des fins distinctes.
useCallback(fn, deps)mémorise l'instance de fonction. Il vous redonne le même objet fonction entre les rendus.useMemo(() => value, deps)mémorise la valeur de retour d'une fonction. Il exécute la fonction et vous redonne son résultat, en le recalculant uniquement lorsque les dépendances changent.
Essentiellement, `useCallback(fn, deps)` n'est qu'un sucre syntaxique pour `useMemo(() => fn, deps)`. Il s'agit d'un hook de commodité pour le cas d'utilisation spécifique des fonctions de mémorisation.
Quand utiliser lequel ?
- Utilisez
useCallbackpour les fonctions que vous transmettez aux composants enfants afin d'éviter les rendus à nouveau inutiles (par exemple, les gestionnaires d'événements tels queonClick,onSubmit). - Utilisez
useMemopour les calculs coûteux, tels que le filtrage d'un grand ensemble de données, les transformations de données complexes ou toute valeur qui prend beaucoup de temps à calculer et ne doit pas être recalculée à chaque rendu.
// Cas d'utilisation pour useMemo : calcul coûteux
const visibleTodos = useMemo(() => {
console.log('Filtrage de la liste...'); // C'est coûteux
return todos.filter(t => t.status === filter);
}, [todos, filter]);
// Cas d'utilisation pour useCallback : gestionnaire d'événements stable
const handleAddTodo = useCallback((text) => {
dispatch({ type: 'ADD_TODO', text });
}, []); // Fonction de dispatch stable
return (
);
Conclusion et meilleures pratiques
Le hook useCallback est un outil puissant dans votre boîte à outils d'optimisation des performances React. Il s'attaque directement au problème de l'égalité référentielle, vous permettant de stabiliser les props de fonction et de libérer tout le potentiel de `React.memo` et d'autres hooks comme `useEffect`.
Principaux points à retenir :
- Objectif :
useCallbackrenvoie une version mémorisée d'une fonction de rappel qui ne change que si l'une de ses dépendances a changé. - Cas d'utilisation principal : Pour éviter les rendus à nouveau inutiles des composants enfants qui sont enveloppés dans
React.memo. - Cas d'utilisation secondaire : Pour fournir une dépendance de fonction stable pour d'autres hooks, tels que
useEffect, pour les empêcher de s'exécuter à chaque rendu. - Le tableau de dépendances est crucial : Incluez toujours toutes les variables de portée de composant dont votre fonction dépend. Utilisez la règle ESLint `exhaustive-deps` pour appliquer cela.
- C'est une optimisation, pas une valeur par défaut : N'enveloppez pas chaque fonction dans
useCallback. Cela peut nuire aux performances et ajouter une complexité inutile. Profilez d'abord votre application et appliquez des optimisations de manière stratégique là où elles sont le plus nécessaires.
En comprenant le « pourquoi » derrière useCallback et en adhérant à ces meilleures pratiques, vous pouvez aller au-delà des conjectures et commencer à apporter des améliorations de performances éclairées et percutantes à vos applications React, en créant des expériences utilisateur qui ne sont pas seulement riches en fonctionnalités, mais aussi fluides et réactives.