Ce n’est un secret pour personne que React.js est devenu très populaire ces dernières années. C’est maintenant la bibliothèque JavaScript de choix pour de nombreux acteurs les plus en vue de l’Internet, y compris Facebook et WhatsApp.

L’une des principales raisons de son essor est l’introduction des hooks dans la version 16.8. Les hooks React vous permettent d’exploiter les fonctionnalités de React sans avoir à écrire des composants de classe. Désormais, les composants fonctionnels avec des hooks sont devenus la structure de prédilection des développeurs pour travailler avec React.

Dans cet article de blog, nous allons approfondir un hook spécifique – useCallback – car il touche à une partie fondamentale de la programmation fonctionnelle connue sous le nom de mémorisation. Vous saurez exactement comment et quand utiliser le hook useCallback et tirer le meilleur parti de ses capacités d’amélioration des performances.

Prêt ? C’est parti !

Qu’est-ce que la mémorisation ?

La mémorisation consiste à ce qu’une fonction complexe stocke sa sortie afin qu’elle soit appelée la prochaine fois avec la même entrée. C’est similaire à la mise en cache, mais à un niveau plus local. Elle permet de sauter les calculs complexes et de renvoyer la sortie plus rapidement puisqu’elle est déjà calculée.

Cela peut avoir un effet significatif sur l’allocation de mémoire et les performances, et cette contrainte est ce que le hook useCallback est censé alléger.

React’s useCallback vs useMemo

À ce stade, il est utile de mentionner que useCallback se combine parfaitement avec un autre hook appelé useMemo. Nous les aborderons tous les deux, mais dans cet article, nous allons nous concentrer sur useCallback comme sujet principal.

La différence essentielle est que useMemo renvoie une valeur mémorisée, alors que useCallback renvoie une fonction mémorisée. Cela signifie que useMemo est utilisé pour stocker une valeur calculée, tandis que useCallback renvoie une fonction que vous pouvez appeler plus tard.

Ces hooks vous renvoient une version en cache, sauf si l’une de leurs dépendances (par exemple, l’état ou les props) change.

Jetons un coup d’oeil aux deux fonctions en action :

import { useMemo, useCallback } from 'react'
const values = [3, 9, 6, 4, 2, 1]

// This will always return the same value, a sorted array. Once the values array changes then this will recompute.
const memoizedValue = useMemo(() => values.sort(), [values])

// This will give me back a function that can be called later on. It will always return the same result unless the values array is modified.
const memoizedFunction = useCallback(() => values.sort(), [values])

L’extrait de code ci-dessus est un exemple artificiel mais montre la différence entre les deux callbacks :

  1. memoizedValue deviendra le tableau [1, 2, 3, 4, 6, 9]. Tant que la variable valeurs reste, il en sera de même pour memoizedValue, et il ne sera jamais recalculé.
  2. memoizedFunction sera une fonction qui retournera le tableau [1, 2, 3, 4, 6, 9].

Ce qui est génial avec ces deux callbacks, c’est qu’ils sont mis en cache et restent en place jusqu’à ce que le tableau de dépendances change. Cela signifie que lors d’un rendu, ils ne seront pas collectés.

Rendu et React

Pourquoi la mémorisation est-elle importante lorsqu’il s’agit de React ?

Cela a à voir avec la façon dont React rend vos composants. React utilise un DOM virtuel stocké en mémoire pour comparer les données et décider de ce qu’il faut mettre à jour.

Le DOM virtuel aide React avec la performance et garde votre application rapide. Par défaut, si une valeur de votre composant change, le composant entier sera rendu à nouveau. Cela rend React « réactif » aux entrées de l’utilisateur et permet de mettre à jour l’écran sans recharger la page.

Vous ne voulez pas rendre votre composant parce que les changements n’affecteront pas ce composant. C’est là que la mémorisation par le biais de useCallback et useMemo s’avère utile.

Lorsque React effectue un nouveau rendu de votre composant, il recrée également les fonctions que vous avez déclarées à l’intérieur de votre composant.

Notez que lorsque vous comparez l’égalité d’une fonction à une autre fonction, elle sera toujours fausse. Parce qu’une fonction est également un objet, elle ne sera égale qu’à elle-même :

// these variables contain the exact same function but they are not equal
const hello = () => console.log('Hello Matt')
const hello2 = () => console.log('Hello Matt')

hello === hello2 // false
hello === hello // true

En d’autres termes, lorsque React rendra à nouveau votre composant, il verra toutes les fonctions déclarées dans votre composant comme étant de nouvelles fonctions.

Ceci est très bien la plupart du temps, et les fonctions simples sont faciles à calculer et n’auront pas d’impact sur les performances. Mais les autres fois, lorsque vous ne voulez pas que la fonction soit vue comme une nouvelle fonction, vous pouvez compter sur useCallback pour vous aider.

Vous pensez peut-être : « Quand est-ce que je ne voudrais pas qu’une fonction soit vue comme une nouvelle fonction ? » Eh bien, il y a certains cas où useCallback est plus logique :

  1. Vous passez la fonction à un autre composant qui est également mémorisé (useMemo)
  2. Votre fonction a un état interne qu’elle doit mémoriser
  3. Votre fonction est une dépendance d’un autre hook, comme useEffect par exemple

Avantages en termes de performances de React useCallback

Lorsque useCallback est utilisé de manière appropriée, il peut contribuer à accélérer votre application et à empêcher les composants d’effectuer un nouveau rendu s’ils n’en ont pas besoin.

Disons, par exemple, que vous avez un composant qui récupère une grande quantité de données et qui est responsable de l’affichage de ces données sous la forme d’un tableau ou d’un graphique, comme ceci :

Graphique à barres généré à l'aide d'un composant React.
Graphique à barres généré à l’aide d’un composant React.

Supposons que le composant parent de la composante de votre visualisation de données se reforme, mais que les props ou l’état modifiés n’affectent pas cette composante. Dans ce cas, vous n’avez probablement pas envie ou besoin de le rendre à nouveau et de récupérer toutes les données. En évitant ce nouveau rendu et cette nouvelle récupération, vous pouvez économiser la bande passante de votre utilisateur et lui offrir une expérience plus fluide.

Inconvénients de React useCallback

Bien que ce hook puisse vous aider à améliorer les performances, il comporte également des écueils. Voici quelques éléments à prendre en compte avant d’utiliser useCallback (et useMemo) :

  • Garbage Collection : Les autres fonctions qui ne sont pas déjà mémorisées seront jetées par React pour libérer de la mémoire.
  • Allocation de mémoire : Comme pour le Garbage Collection, plus vous avez de fonctions mémorisées, plus vous aurez besoin de mémoire. De plus, chaque fois que vous utilisez ces rappels, il y a un tas de code dans React qui doit utiliser encore plus de mémoire pour vous fournir la sortie en cache.
  • Complexité du code : Lorsque vous commencez à envelopper des fonctions dans ces hooks, vous augmentez immédiatement la complexité de votre code. Il faut maintenant mieux comprendre pourquoi ces hooks sont utilisés et confirmer qu’ils sont utilisés correctement.

Si vous êtes conscient des pièges mentionnés ci-dessus, cela vous évitera de tomber dessus par hasard. Si vous envisagez d’utiliser useCallback, assurez-vous que les avantages en termes de performances l’emportent sur les inconvénients.

Exemple de React useCallback

Voici une configuration simple avec un composant Button et un composant Counter. Le compteur possède deux éléments d’état et rend deux composants bouton, chacun mettant à jour une partie distincte de l’état du composant de compteur.

Le composant Button reçoit deux props : handleClick et name. Chaque fois que le bouton est rendu, il envoie un journal à la console.

import { useCallback, useState } from 'react'

const Button = ({handleClick, name}) => {
  console.log(`${name} rendered`)
  return <button onClick={handleClick}>{name}</button>
}

const Counter = () => {

console.log('counter rendered')
  const [countOne, setCountOne] = useState(0)
  const [countTwo, setCountTwo] = useState(0)
  return (
    <>
      {countOne} {countTwo}
      <Button handleClick={() => setCountOne(countOne + 1)} name="button1" />
      <Button handleClick={() => setCountTwo(countTwo + 1)} name="button1" />
    </>
  )
}

Dans cet exemple, chaque fois que vous cliquez sur l’un des boutons, vous verrez ceci dans la console :

// counter rendered

// button1 rendered
// button2 rendered

Maintenant, si nous appliquons useCallback à nos fonctions handleClick et enveloppons notre bouton dans React.memo, nous pouvons voir ce que useCallback nous apporte. React.memo est similaire à useMemo et nous permet de mémoriser un composant.

import { useCallback, useState } from 'react'

const Button = React.memo(({handleClick, name}) => {
  console.log(`${name} rendered`)
  return <button onClick={handleClick}>{name}</button>
})

const Counter = () => {
  console.log('counter rendered')
  const [countOne, setCountOne] = useState(0)
  const [countTwo, setCountTwo] = useState(0)
  const memoizedSetCountOne = useCallback(() => setCountOne(countOne + 1), [countOne)
  const memoizedSetCountTwo = useCallback(() => setCountTwo(countTwo + 1), [countTwo])
  return (
    <>
        {countOne} {countTwo}
        <Button handleClick={memoizedSetCountOne} name="button1" />
        <Button handleClick={memoizedSetCountTwo} name="button1" />
    </>
  )
}

Maintenant, lorsque nous cliquons sur l’un ou l’autre des boutons, nous ne verrons que le bouton sur lequel nous avons cliqué pour nous connecter à la console :

// counter rendered

// button1 rendered

// counter rendered

// button2 rendered

Nous avons appliqué la mémorisation à notre composant bouton, et les valeurs prop qui lui sont passées sont vues comme égales. Les deux fonctions handleClick sont mises en cache et seront considérées comme la même fonction par React jusqu’à ce que la valeur d’un élément du tableau de dépendances change (par exemple, countOne, countTwo).

Résumé

Aussi cool que soient useCallback et useMemo, rappelez-vous qu’ils ont des cas d’utilisation spécifiques – vous ne devriez pas envelopper chaque fonction avec ces hooks. Si la fonction est complexe du point de vue du calcul, une dépendance d’un autre hook ou une prop passée à un composant mémorisé sont de bons indicateurs que vous pourriez vouloir atteindre useCallback.

Nous espérons que cet article vous a aidé à comprendre cette fonctionnalité avancée de React et vous a permis de gagner en confiance avec la programmation fonctionnelle en cours de route !

Matthew Sobieray

Matthew works for Kinsta as a Development Team Lead from his home office in Denver, Colorado. He loves to learn, especially when it comes to web development.