Tu n'as PAS besoin de useCallback en React
15/10/2024 • Melvynx
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
.
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.
useCallback
?La majorité des cas, on utilise useCallback
pour stabiliser une fonction qui est utilisée dans 2 cas :
useEffect
memo()
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 :
GiantCounter
avec un while
memo
autour de mon GiantCounter
pour éviter qu'il render[ { "code": "invalid_type", "expected": "number", "received": "string", "path": [ "height" ], "message": "Expected number, received string" } ]
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 :
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 :
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 :
[ { "code": "invalid_type", "expected": "number", "received": "string", "path": [ "height" ], "message": "Expected number, received string" } ]
Ce premier usage est de loin le plus utilisé de useCallback
. Il y a déjà une leçon qu'on a apprise :
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 :
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
:
/* 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 :
ref
ref
(avec un useEffect)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.
[ { "code": "invalid_type", "expected": "number", "received": "string", "path": [ "height" ], "message": "Expected number, received string" } ]
Mais j'ai une nouvelle encore pire... généralement tu n'as même pas besoin useEvent
!
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 :
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="mb-1 items-center text-sm font-medium text-gray-200">
{label}
</label>
<div className="flex items-center rounded-lg">
<input
// pleins de props
/>
<span className="cursor-default text-4xl text-gray-100">
{suffix}
</span>
</div>
</div>
);
},
);
NumericInput.displayName = "NumericInput";
export default React.memo(NumericInput);
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
?
Si les deux cas ne sont pas remplis : tu n'as pas besoin de memo
.
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 :