Menu

Le hook useCallback de React : quand il aide vraiment

useCallback renvoie la même fonction d'un rendu à l'autre jusqu'à ce que ses dépendances changent. Il n'aide que lorsque cette fonction est passée à un enfant memo ou utilisée comme dépendance d'un effet. Découvrez la syntaxe, la différence avec useMemo et les erreurs à éviter.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

useCallback met en cache une fonction entre les rendus : il renvoie le même objet fonction jusqu'à ce qu'une de ses dépendances change. Cela ne compte que lorsque quelque chose compare la fonction par référence, c'est-à-dire en pratique un enfant enveloppé dans memo ou un effet qui liste la fonction comme dépendance.

Les deux boutons sont mémoïsés et font la même chose. Basculez le thème et regardez la console : seul render Inline function apparaît, car saveInline est une nouvelle fonction à chaque rendu, donc memo voit une prop modifiée. saveCached est la même fonction à chaque fois, et ce bouton est sauté.

Ce qu'il met en cache

const cachedFn = useCallback(fn, dependencies);
  • fn est la fonction que vous voulez garder. React ne l'appelle pas ; il vous la renvoie.
  • dependencies liste chaque valeur du composant que fn lit. À chaque rendu, React les compare aux précédentes avec Object.is.
  • Si rien n'a changé, vous obtenez la fonction du rendu précédent. Si quelque chose a changé, vous obtenez la fonction passée cette fois, et React la garde pour la fois suivante.

Notez ce que cela implique : la fonction fléchée dans useCallback est quand même créée à chaque rendu, car c'est simplement un argument. useCallback n'épargne pas ce travail. Son seul rôle est de renvoyer une fonction plus ancienne et identique pour que la référence reste la même.

useCallback ou useMemo

C'est le même mécanisme. useMemo met en cache le résultat de l'appel d'une fonction, useCallback met en cache la fonction elle-même :

// These two lines are equivalent
const handleSave = useCallback(() => save(draft), [draft]);
const handleSave = useMemo(() => () => save(draft), [draft]);

Utilisez useCallback pour les fonctions, car c'est plus lisible, et useMemo pour les valeurs calculées comme une liste filtrée ou un objet d'options.

Supprimer des dépendances avec les fonctions de mise à jour

Une fonction en cache doit lister chaque valeur d'état qu'elle lit, et chaque fois que cet état change, la fonction change aussi. Pour une liste de lignes mémoïsées, c'est un problème : une fonction remove qui lit todos change chaque fois que todos change, donc chaque ligne restante refait son rendu après chaque suppression. Utilisez la forme fonction de mise à jour du setter pour que la fonction ne lise plus du tout todos.

Supprimez une ligne : la console reste silencieuse, car les autres lignes ont reçu le même objet todo et la même fonction remove qu'avant. Remplacez la fonction par useCallback((id) => setTodos(todos.filter((t) => t.id !== id)), [todos]) et réessayez : chaque ligne restante affiche maintenant un rendu après chaque suppression.

Les fonctions comme dépendances d'effet

Un effet qui appelle une fonction déclarée dans le composant doit lister cette fonction comme dépendance. Si la fonction est nouvelle à chaque rendu, l'effet s'exécute après chaque rendu. useCallback la garde stable jusqu'à ce que ses propres entrées changent :

Taper un message ne provoque pas de reconnexion. Changer de salon affiche une déconnexion de l'ancien salon et une connexion au nouveau. Supprimez l'enveloppe useCallback (gardez la fonction fléchée) et chaque frappe déconnecte puis reconnecte.

Il existe pourtant une solution plus simple ici : déplacer la fonction dans l'effet. L'effet dépend alors directement de roomId et aucune fonction n'a besoin d'être stable.

useEffect(() => {
    function createOptions() {
        return { serverUrl: 'wss://chat.example', roomId };
    }
    const options = createOptions();
    // ...
}, [roomId]);

Préférez cela quand la fonction n'est utilisée que par l'effet. Gardez useCallback pour les fonctions que vous passez aussi vers le bas ou que vous renvoyez depuis un hook personnalisé, là où les appelants pourraient les placer dans leurs propres tableaux de dépendances.

Vérifier si une fonction a changé

Quand un enfant memo continue de refaire son rendu et que vous ne savez pas pourquoi, vérifiez si la prop fonction est vraiment le même objet d'un rendu à l'autre. Gardez la précédente dans une ref et comparez :

Cliquez sur Count : la console indique increment changed: false, car step n'a pas changé. Cliquez sur Step et elle indique true : step est une dépendance, donc React a renvoyé la nouvelle fonction. Une fonction qui change à chaque rendu signifie généralement qu'une dépendance est elle-même nouvelle à chaque rendu, comme un objet ou une autre fonction déclarée dans le composant.

useCallback dans les hooks personnalisés

Un hook personnalisé qui renvoie des fonctions est un bon endroit pour useCallback, même si vous ne savez pas qui l'appellera. Le composant qui utilise le hook peut passer ces fonctions à un enfant memo ou les lister dans un effet, et il ne peut pas les rendre stables de l'extérieur. Les envelopper dans le hook donne par défaut une référence stable à chaque appelant :

function useCounter(initial = 0) {
    const [count, setCount] = useState(initial);
    const increment = useCallback(() => setCount((c) => c + 1), []);
    const reset = useCallback(() => setCount(initial), [initial]);
    return { count, increment, reset };
}

L'erreur courante : tout envelopper

useCallback autour d'un gestionnaire qui va directement à un <button> ou un <input> ne sert à rien. React n'épargne aucun travail pour un élément DOM parce que son gestionnaire est resté le même, et un composant qui n'est pas enveloppé dans memo refait de toute façon son rendu quand son parent le fait.

// Pointless: <button> does not care that the function is the same
const handleClick = useCallback(() => setOpen(true), []);
return <button onClick={handleClick}>Open</button>;

Chaque useCallback supplémentaire ajoute un tableau de dépendances qu'on peut se tromper à écrire, et une dépendance périmée est un vrai bug (la fonction continue de lire un ancien état). Ajoutez-le quand vous avez un enfant memo ou un tableau de dépendances du côté qui reçoit la fonction, pas par défaut.

React Compiler 1.0, quand votre build l'utilise, mémoïse automatiquement les fonctions et les valeurs, ce qui supprime le besoin de la plupart des appels à useCallback écrits à la main. Sans le compilateur, les règles ci-dessus s'appliquent toujours, et useCallback écrit à la main reste courant dans le code existant.

Questions fréquentes

Que fait useCallback ?

Il renvoie le même objet fonction à chaque rendu jusqu'à ce qu'une de ses dépendances change. Il n'empêche pas la création de la fonction ; il renvoie la fonction d'un rendu précédent au lieu de la nouvelle.

Quand utiliser useCallback ?

Quand vous passez une fonction à un enfant enveloppé dans memo, ou quand la fonction est une dépendance d'un effet ou d'un autre hook. Dans les deux cas, une nouvelle fonction à chaque rendu annulerait l'optimisation ou relancerait l'effet.

Quelle est la différence entre useCallback et useMemo ?

useCallback(fn, deps) met en cache fn elle-même. useMemo(() => value, deps) met en cache la valeur que renvoie une fonction. useCallback(fn, deps) équivaut exactement à useMemo(() => fn, deps).

Faut-il envelopper chaque fonction dans useCallback ?

Non. Si la fonction va à un simple élément DOM ou à un composant qui n'est pas mémoïsé, une référence stable ne change rien, et les tableaux de dépendances supplémentaires rendent le code plus difficile à lire et plus facile à casser.

useCallback rend-il mon composant plus rapide ?

Pas à lui seul. Il n'aide que lorsque quelque chose en aval compare la fonction par référence, comme memo ou le tableau de dépendances d'un effet.

Illustration des langages de programmation de Coddy

Apprendre à coder avec Coddy

COMMENCER