React.memo vs React.useCallback vs React.useMemo

September 07, 2024

Introduction

Nous allons découvrir dans ce post 3 hooks permettant d’améliorer les performances d’une application React, en limitant le nombre de render pour React.memo et React.useCallback, et en cachant le résultat d’une fonction couteuse avec React.useMemo.

Update du state à partir du parent: React.memo

Imaginons un cas très simple, où un composant Parent met à jour sont état via un useState. Si ce composant Parent possède des enfants Child, alors ceux-ci seront également rendus, même s’ils ne sont pas concernés pas la mise à jour de l’état (c’est à dire qu’aucune props de Child ne change).

Pour tester simplement, on aura donc un componsant Parent avec un state, et un bouton de mise à jour de ce state :

const [count, setCount] = useState(0)
...
<button onClick={() => setCount(count + 1)}>Click Me!</button>

Le composant Parent rendera un composant Child tout simple:

<Child  />

On va ajouter un console.log dans le retour de chaque composant (à base de fonction) pour vérifier le comportement.

Problème

On se rend compte qu’au click sur le bouton, le composant Child est rendu! Or il n’y a pas lieu d’être : on a juste mis un jour le state de Parent, et rien n’est passé à Child via les props! Aucune raison de rendre de nouveau Child!

Solution : React.memo

Une solution à ce problème est simplement de déclarer notre composant Child avec memo : cela indique à React que si les props du composant Child n’ont pas changé, alors ne pas le rendre à nouveau.

  const Child = React.memo(() => {
...
   })

Ainsi le composant Child ne sera pluse rendu au click sur le bouton “Click Me”.

Update du state à partir de l’enfant: React.useCallback

Imaginons un autre cas, où cette fois, un composant enfant Child a besoin de mettre à jour l’état de son parent (Parent). On passera donc une fonction du Parent vers le Child, via une prop.

Comme dans l’exemple précédent, soit un composant Parent avec un state très simple, un compteur count:

const [count, setCount] = useState(0)

la fonction qui incrémente le compteur et qui sera passée à Child:

 const handleClick = () =>{
    setCount(count+1)  
  }

Passée via une prop à Child:

<Child handleClick={handleClick} />

Enfin j’affiche la valeur de count dans mon Parent :

count is {count}

Dans mon Child, j’aurai un bouton simple pour appeler la fonction handleClick :

<button onClick={handleClick}>update from child</button>

Problème

Et bien si on laisse tel quel, au click sur le bouton, le composant Child va être rendu de nouveau! Pourtant c’est inutile, vu que la valeur de count est uniquement affichée dans le Parent, pourquoi re-render Child? C’est une pure perte de performance!

React agit ainsi, car la fonction handleClick aura une nouvelle référence à chaque rendu. Pour éviter cela, il faut utiliser useCallback.

Solution

Nous allons simplement changer la fonction handleClick avec :

const handleClick = useCallback(() => {
    setCount(count + 1)
  }, [])

En spécifiant useCallback avec un tableau vide, on indique à React que la fonction handleClick a besoin d’être instanciée une seule fois, à l’initialisation du composant : le composant Child ne sera pas rendu de nouveau, et on a corrigé notre problème!

Mettre en cache un résultat couteux: React.useMemo

Cette fois-ci, on ne va pas pas se baser sur le rendu des composants Parent et Enfant, mais plutôt, imaginons une fonction trés lente (calcul complexes par exemple) dans un seul et même composant Parent :

const computedValue = () => {
  console.log('compute value')
  let num=0;
  for (let i = 0; i < 1000000000; i++) {
    num += 1;
  }
  return num;
  };

Cette fonction n’a aucune dépendance sur un quelconque state.

Ajoutons en plus un state de compteur, comme fait précédemment dans le composant Parent :

const [count, setCount] = useState(0);

Ajoutons un bouton pour mettre à jour le state count :

<button onClick={() => setCount(count + 1)}>Click Me!</button>

Afficher le résultat de computedValue :

<h1>Hello {computedValue()}</h1>

Problème

Quand vous aller clicker sur le bouton, la fonction très lente computedValue va AUSSI être executée! En effet, le state change, et donc par défaut React execute de nouveau la fonction couteuse.

Solution

Il faut indiquer à React que, vu que la fonction ne dépend pas d’un state, ne pas recalculer celle-ci. Avec useMemo ça donne :

 const computedValue = useMemo(() => {
    console.log('compute value')
    let num=0;
    for (let i = 0; i < 1000000000; i++) {
      num += 1;
    }
    return num;
  }, []);

Il faut aussi changer l’appel à computedValue suite à l’utilisation de useMemo, en enlevant les parenthèses:

<h1>Hello {computedValue}</h1>

Et là bingo, on ne rentre plus dans la fonction computedValue() au click sur le bouton, et donc du changement de state de count! On a mis en place un cache pour cette fonction.

J’espère que ces petis exemples vous autont familiarisé avec ces hooks fondamentaux de React. Comme d’habitude le code est ici sur mon Github. Changer le cas voulu dans main.tsx, puis lancer npm i et npm run dev.

Voici également un article qui m’a inspiré : https://kaushaldhakal40.medium.com/optimizing-react-performance-preventing-unnecessary-child-component-re-renders-17b421a6d39e#:~:text=The%20Solution%3A%20Memoization%20with%20React,change%2C%20effectively%20preventing%20unnecessary%20renders.

Codez bien!


Ecrit par Sylvain Maestri qui vit et travaille à Paris, et qui aime construire des applications WEB avec les langages Java & Javascript, et appliquer les bons patterns de programmation SOLID, TDD, BDD et DDD.