codelynx.dev
🇫🇷🇬🇧

Retour 15/10/2024

Tu n'as PAS besoin de useCallback en React

Écris par Melvyn Malherbe le 15/10/2024


Quand je rentre dans un codebase ou copie le code d'un projet Shadcn/UI je vois plein de useCallback et après quelques minutes de réflexion, je me retrouve toujours à les supprimer.

Tu n'as généralement jamais besoin de useCallback

On va voir ensemble quand tu en as vraiment besoin et quand l'utiliser et plusieurs exemples de mauvaise utilisation de useCallback.

C'est quoi useCallback ?

useCallback est un hook React qui permet de stabiliser une fonction. Il faut comprendre que la référence d'une méthode change par défaut à chaque rendu de ton composant. Avec useCallback, elle ne change QUE quand le tableau de dépendances change.

Quand utiliser useCallback ?

La majorité des cas, on utilise useCallback pour stabiliser une fonction qui est utilisée dans 2 cas :

  1. Dans les dépendances d'un useEffect
  2. Dans les dépendances d'un memo()

Utilisation avec memo()

Le cas très courant est d'avoir un composant qui possède un state et un nombre composant, que j'ai appelé GiantCounter qui est "long" et compliqué à charger pour React.

Dans le playground suivant :

  1. J'ai simulé un lag dans le composant GiantCounter avec un while
  2. J'ai mis memo autour de mon GiantCounter pour éviter qu'il render

memo permet de limiter les render d'un composant uniquement quand celui-ci a des props différentes.

Le problème que tu remarques c'est que mon composant render toujours, on peut le voir car l'application bug. C'est car la fonction onBigButtonClick est redéfinie à chaque render.

C'est difficile à comprendre, car elle ne change pas en soi mais elle est bien redéfinie et remplacée par une fonction similaire.

Tu peux le comprendre ici :

On peut imaginer en quelque sorte que la méthode est constamment redéfinie. Ce problème peut-être résolu en utilisant useCallback mais pas de cette manière :

JS
const onBigButtonClick = useCallback(() => {
  setCount(count + 1000);
}, [count]);

Ici le problème reste le même, car count est constamment différent, il faut être encore plus malin et faire ceci :

JS
const onBigButtonClick = useCallback(() => {
  setCount((c) => c + 1000);
}, []);

Ici, notre méthode ne va jamais changer et "son id" restera stable. Tu peux voir que maintenant notre application ne bug plus :

Ce premier usage est de loin le plus utilisé de useCallback. Il y a déjà une leçon qu'on a apprise :

Leçon 1 : Utiliser useCallback sans memo est inutile

Beaucoup de personnes utilisent des useCallback alors même que le composant qui prend les paramètres n'est pas memo. Il faut savoir que c'est totalement inutile. Car par défaut React suit la règle suivante :

  • Quand un composant est rendu, tous ses enfants sont rendus aussi (sauf s'ils sont mémorisés)

useEvent

Mais j'ai une nouvelle encore pire... Même ici, l'utilisation de useCallback est inutile.

Il y a une méthode magique que je dois te partager et ça s'appelle useEvent, ici notre méthode onBigButtonClick est un event, il vient être exécuté à un moment et c'est tout.

On peut donc wrapper ce callback dans useEvent :

JS
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useCallback, useEffect, useRef } from 'react';

type EventCallback<T extends any[]> = (...args: T) => void;

export const useEvent = <T extends any[]>(
  callback: EventCallback<T>
): EventCallback<T> => {
  const ref = useRef<EventCallback<T> | null>(null);

  useEffect(() => {
    ref.current = callback;
  }, [callback]);

  // Use useCallback to memoize the returned function
  const triggerEvent = useCallback((...args: T) => {
    ref.current?.(...args);
  }, []);

  return triggerEvent;
};

Cette méthode vient :

  1. Sauvegarder le callback dans un ref
  2. Chaque fois que le callback change, on met à jour la ref (avec un useEffect)
  3. On retourne une fonction qui va appeler le callback (qui utilise useCallback avec aucun paramètre)

Si tu ne comprends pas comment est utilisé useRef tu peux aller voir mon article.

Résultat ? On retourne une méthode 100% stable qui ne va jamais changer. Avec cette solution il est même possible d'utiliser count + 1000 dans le callback car celui-ci est une référence 100% stable.

Mais j'ai une nouvelle encore pire... généralement tu n'as même pas besoin useEvent !

Pas de memo()

Ce qu'il faut challenger en premier, c'est l'utilisation abusive de memo(). Un de mes élèves a utilisé memo() pour ce composant :

TSX
const NumericInput = forwardRef<HTMLInputElement, NumericInputProps>(
  ({ min, max, label, suffix = '', onNavigate, onEnterPress }, ref) => {
    const timeUnit = shortLabelToTimeUnit[label as ShortLabel];

    const { value, handleKeyDown, handleChange } = useNumericInputLogic({
      min,
      max,
      label: timeUnit,
      onNavigate,
      onEnterPress,
    });

    const { handleFocus, handleBlur, isFocused } = useFocusClass(
      ref as React.RefObject<HTMLInputElement>,
      'bg-[#894889]/70 text-white'
    );

    return (
      <div className="flex flex-col items-center">
        <label className="items-center mb-1 font-medium text-gray-200 text-sm">
          {label}
        </label>
        <div className="flex items-center rounded-lg">
          <input
          // pleins de props
          />
          <span className="text-4xl text-gray-100 cursor-default">{suffix}</span>
        </div>
      </div>
    );
  }
);

NumericInput.displayName = 'NumericInput';

export default React.memo(NumericInput);
Ce composant n'a PAS besoin de memo !

Pour être honnête, il y en a très peu qui en ont besoin. C'est plus une anomalie qu'un pattern standard.

Les renders ne sont pas mauvais !

Il est normal que ton composant render souvent même s'il n'en a pas besoin, c'est la manière de React de faire en sorte que ton application soit toujours à jour.

Le problème c'est quand les render font laguer ton application ou sont trop à faire. Si ton render a un temps normal pour être effectué, tu n'as pas besoin de memo et donc souvent, pas besoin de useCallback.

Quand utiliser memo ?

  • Quand un composant est lourd (souvent qu'il rend beaucoup de composants enfants)
  • Quand un composant est render très souvent pour rien

Si les deux cas ne sont pas remplis : tu n'as pas besoin de memo.

Conclusion

useCallback est très souvent utilisé pour éviter que des composants mémorisés soient render... mais souvent il n'y a même pas besoin d'utiliser memo.

Tous ces patterns c'est le genre de truc que je parle dans ma formation React que tu peux retrouver ici :

Le meilleur moyen d'apprendre React !

Rejoins par développeurs, cette formation reçoit une note de 4.7 / 5 🚀

Reçois 12 leçons premium pour maîtriser React et faire partie des meilleurs

BeginReact

Cours React gratuit

Accède à des exercices, des vidéos et bien plus sur React dans la formation "BeginReact" 👇