
`useCallback` : Mémoïsation des fonctions callback
Apprenez à utiliser le hook useCallback en React pour mémoïser les fonctions callback et éviter des re-rendus inutiles d'enfants mémoïsés.
Le problème : Nouvelles fonctions à chaque rendu
Dans les composants fonctionnels React, lorsque vous définissez une fonction à l'intérieur du corps du composant (par exemple, un gestionnaire d'événements comme `handleClick` ou une fonction passée comme prop à un enfant), cette fonction est techniquement recréée à chaque rendu du composant parent.
Même si le code de la fonction est identique d'un rendu à l'autre, la référence en mémoire de la fonction elle-même change. Cela pose un problème similaire à celui des objets ou tableaux littéraux lorsqu'on travaille avec des optimisations basées sur la comparaison de référence, comme `React.memo` ou les tableaux de dépendances de `useEffect` ou `useMemo`.
Si vous passez une fonction définie directement dans le parent comme prop à un composant enfant mémoïsé avec `React.memo`, l'enfant se re-rendra inutilement à chaque rendu du parent, car la prop fonction aura une nouvelle référence à chaque fois, faisant échouer la comparaison superficielle de `React.memo`.
Le hook `useCallback` est conçu pour résoudre ce problème spécifique en mémoïsant la fonction elle-même.
Qu'est-ce que `useCallback` et comment ça marche ?
Le hook `useCallback` est très similaire à `useMemo`, mais au lieu de mémoïser une valeur de retour, il mémoïse la fonction elle-même. Il prend deux arguments :
- La fonction callback que vous souhaitez mémoïser.
- Un tableau de dépendances.
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);Le fonctionnement est le suivant :
- Lors du premier rendu, `useCallback` retourne la fonction callback que vous lui avez passée et la mémorise.
- Lors des rendus suivants, React compare les valeurs actuelles des dépendances (`[a, b]`) avec celles du rendu précédent.
- Si aucune des dépendances n'a changé (comparaison `Object.is`), React retourne la même instance (référence) de la fonction qu'au rendu précédent.
- Si au moins une des dépendances a changé, React recrée la fonction callback, la mémorise, et retourne cette nouvelle instance.
En bref, `useCallback(fn, deps)` est équivalent à `useMemo(() => fn, deps)`. Il garantit que la référence de la fonction ne change que si ses dépendances changent.
Cas d'usage principal : Passer des callbacks stables à des enfants optimisés
L'utilité première de `useCallback` est d'optimiser les performances lorsque vous passez des fonctions callback comme props à des composants enfants qui sont mémoïsés avec `React.memo` (ou qui utilisent ces callbacks dans leurs propres tableaux de dépendances `useEffect`/`useMemo`).
Reprenons l'exemple de `React.memo` où nous passions une fonction `onClick` :
import React, { useState, useCallback } from 'react';
const BoutonEnfant = React.memo(function BoutonEnfant({ onClick, children }) {
console.log(`Rendu BoutonEnfant : ${children}`);
return ;
});
function CompteurParent() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
console.log('Rendu CompteurParent');
// Sans useCallback, handleClick1 serait une nouvelle fonction à chaque rendu
// const handleClick1 = () => setCount1(c => c + 1);
// Avec useCallback, handleClick1 garde la même référence tant que setCount1 ne change pas (ce qui n'arrive jamais)
const handleClick1 = useCallback(() => {
setCount1(c => c + 1);
}, []); // Aucune dépendance externe dans le callback
// handleClick2 dépend de count1, donc il sera recréé si count1 change
const handleClick2 = useCallback(() => {
console.log(`Count1 actuel: ${count1}`);
setCount2(c => c + 1);
}, [count1]); // Dépendance: count1
return (
Compteur 1: {count1}
Compteur 2: {count2}
Incrémenter Compteur 1
Incrémenter Compteur 2 (dépend de C1)
);
}
export default CompteurParent;Ici :
- `handleClick1` est mémoïsé avec `useCallback` et un tableau de dépendances vide (`[]`) car il n'utilise aucune variable ou fonction de la portée du composant (sauf `setCount1` qui est garantie stable par React). Sa référence ne changera jamais.
- `handleClick2` est aussi mémoïsé, mais il dépend de `count1`. Sa référence ne changera que si `count1` change.
- Les composants `BoutonEnfant` sont mémoïsés avec `React.memo`.
Résultat :
- Quand on clique sur "Incrémenter Compteur 1", `count1` change. `CompteurParent` se re-rend. `handleClick1` garde la même référence. `handleClick2` obtient une nouvelle référence (car `count1` a changé). Le premier `BoutonEnfant` reçoit la même référence `onClick`, donc `React.memo` saute son rendu. Le second `BoutonEnfant` reçoit une nouvelle référence `onClick`, donc il se re-rend.
- Quand on clique sur "Incrémenter Compteur 2", `count2` change. `CompteurParent` se re-rend. `handleClick1` garde la même référence. `handleClick2` garde aussi la même référence (car `count1` n'a pas changé). Les deux `BoutonEnfant` reçoivent la même référence `onClick`, donc `React.memo` saute leurs rendus.
Sans `useCallback`, les deux boutons se seraient re-rendus à chaque clic sur l'un ou l'autre.
Autres cas d'usage et règles des dépendances
Outre l'optimisation des enfants mémoïsés, `useCallback` peut aussi être utile lorsque vous passez une fonction comme dépendance à un autre hook (comme `useEffect`). Si vous ne mémoïsez pas la fonction, l'effet pourrait se déclencher à chaque rendu car la référence de la fonction changerait.
function MonComposant({ termeRecherche }) {
const [data, setData] = useState(null);
// Sans useCallback, fetchData serait recréée à chaque rendu,
// déclenchant potentiellement useEffect à chaque fois.
const fetchData = useCallback(async () => {
const result = await api.search(termeRecherche);
setData(result);
}, [termeRecherche]); // Dépend de termeRecherche
useEffect(() => {
fetchData();
}, [fetchData]); // L'effet dépend de la fonction fetchData mémoïsée
// ...
}Règles des dépendances : Comme pour `useEffect` et `useMemo`, vous devez inclure dans le tableau de dépendances de `useCallback` toutes les valeurs (props, état, variables locales) provenant de la portée du composant qui sont utilisées à l'intérieur de la fonction callback. Omettre une dépendance peut conduire à capturer des valeurs obsolètes (stale closure). L'outil ESLint vous aidera à identifier les dépendances manquantes.
Optimisation prématurée : Tout comme `useMemo`, `useCallback` a un coût (mémoire, comparaison). Ne l'utilisez pas systématiquement pour chaque fonction. Son principal intérêt réside dans l'optimisation de composants enfants via `React.memo` ou lorsqu'une fonction est utilisée comme dépendance d'un autre hook. Si la fonction n'est pas passée en prop ou utilisée comme dépendance, la mémoïsation n'apporte généralement aucun bénéfice et ajoute une complexité inutile.
En conclusion, `useCallback` est le hook jumeau de `useMemo`, spécifiquement conçu pour mémoïser les fonctions callbacks. Il est essentiel pour préserver l'égalité référentielle des fonctions passées en props à des composants optimisés ou utilisées comme dépendances d'autres hooks, contribuant ainsi à éviter des re-rendus ou des exécutions d'effets inutiles.