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 :
useEffectmemo()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 whilememo 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 :
refref (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 :