Optimisation et gestion de context avec les Hooks

Rémy Codron
Just-Tech-IT
Published in
5 min readJun 8, 2020

React fournit un ensemble d’outils permettant de simplifier la conception des applications :
- Les Hooks ont simplifié l’écriture des composants.
- L’API Context nous permet d’avoir un (ou plusieurs) store sur notre application
- La mémoïsation des composants permet de palier à des problèmes de performances en retournant la valeur d’un composant sans devoir le render à nouveau.

L’utilisation des Hooks et de l’API Context peut très rapidement conduire à des problèmes de performances que la mémoïsation ne pourrait pas toujours régler.
Cependant mis bout à bout ces 3 notions de React vont quand même nous permettre de créer des applications simples et optimisées.

Prenons un exemple simple

Un exemple classique d’un compteur affiche une donnée stockée dans le store de l’application. Au clic sur l’un des boutons la donnée et le composant se mettent à jour.

On ne constatera bien évidemment aucun problème de performance ici.

Complexifions un peu plus l’exemple

En complexifiant plus l’application on peut très rapidement voir apparaître des problèmes de performance.

Prenons comme exemple une liste quelconque affichant des centaines d’éléments. On voudrait qu’au clic sur un élément celui-ci passe en gras. Le problème qu’on va rapidement constater est que l’ensemble de la liste va se rafraîchir là où seulement l’ancien élément et le nouvel élément sélectionné ont besoin d’être rechargé.

il est possible de voir le render de chaque élément dans la console de debug

Sur cet exemple à chaque clic sur un élément l’ensemble de la liste se rafraîchie.
NB : il est possible de modifier le nombre d’élément à afficher dans App.js pour le passer de 100 à 10000 pour constater les problèmes de performances.

On peut observer ce render global au clic sur un Item dans le profiler :

Le composant List et tous les composants Item sont re-rendu

Le temps de render de l’ensemble de l’application est de 25ms.

Que se passe-t-il ?

Tout se passe dans l’exposition des propriétés du context :

import React, { createContext, useState } from "react";
import PropTypes from "prop-types";
export const Store = createContext({});export const StoreProvider = props => {
const { children } = props;
const [items, setItems] = useState([]);
const [currentItem, setCurrentItem] = useState();
const appContext = {
items,
setItems,
currentItem,
setCurrentItem
};
return <Store.Provider value={appContext}>{children}</Store.Provider>;
};
StoreProvider.propTypes = {
children: PropTypes.any
};

Le context expose un objet comportant 4 propriétés :

{
items,
setItems,
currentItem,
setCurrentItem
}

Dès qu’une de ces propriétés est mise à jour, React considère que le context a été modifié et met à jour tous les composants enfants.

Le composant Item (components/Item/Item.js) utilise le store pour indiquer quel élément est sélectionné dans la liste.

const { setCurrentItem } = useContext(Store);

Il ne devrait pas se mettre à jour, il n’attend aucune nouvelle valeur à part une props isCurrent lui indiquant si il est sélectionné ou non.

On se retrouve avec cette mise à jour en cascade :

Que devrait-il se passer ?

Dans un cas optimal on ne devrait mettre à jour que les éléments qui ont réellement besoin de données fraîches :
- le composant List qui doit connaitre l’item sélectionné
- le nouvel item sélectionné
- l’ancien item sélectionné

Plusieurs approches sont possibles pour arriver à ce résultat.

Passer les méthodes du context en props

Une première approche pourrait être de passer en props la méthode setCurrentItem depuis le composant List aux composants Item enfants

On peut voir dans le profiler que cette méthode fonctionne :

l’item sélectionné et l’ancien item sont mis à jour, les autres non

Le temps de render de l’ensemble de l’application est de 3ms.

Malheureusement en passant des méthodes du context en props de certains composants on perd tout l’intérêt d’avoir un store global. Une application plus complexe pourrait très vite devenir difficile à maintenir.

On remarque également que le composant App se met à jour alors qu’il n’en a pas besoin.

Memoïser le Context

Une seconde approche serait de s’intéresser au context pour optimiser notre application :

import React, { createContext, useState } from "react";
import PropTypes from "prop-types";
export const Store = createContext({});export const StoreProvider = props => {
const { children } = props;
const [items, setItems] = useState([]);
const [currentItem, setCurrentItem] = useState();
const appContext = {
items,
setItems,
currentItem,
setCurrentItem
};
return <Store.Provider value={appContext}>{children}</Store.Provider>;
};
StoreProvider.propTypes = {
children: PropTypes.any
};

Nous avons 2 types de propriétés dans notre Context :

  • les states : items, currentItem
  • les méthodes de mise à jour : setItems, setCurrentItem

La solution serait de mémoïser ces méthodes pour que React ne déclenche pas un render des composants les utilisant :

On isole et mémoïse les méthodes qui n’ont pas besoin de déclencher un render des composants :

import React, { createContext, useState } from "react";
import PropTypes from "prop-types";
export const Store = createContext({});
export const StoreActions = createContext({});
export const StoreProvider = props => {
const { children } = props;
const [items, setItems] = useState([]);
const [currentItem, setCurrentItem] = useState();
const actions = React.useMemo(
() => ({
setItems,
setCurrentItem
}),
[]
);
const states = {
items,
currentItem
};
return (
<StoreActions.Provider value={actions}>
<Store.Provider value={states}>{children}</Store.Provider>
</StoreActions.Provider>
);
};
StoreProvider.propTypes = {
children: PropTypes.any
};

On peut voir dans le profiler que seul le composant List et les 2 composants Item concernés sont rendus. Les composants App et les autres composants Item n’ont plus besoin d’être mis à jour :

Seuls les composants utilisant des données à jour sont mis à jour

Le temps de render de l’ensemble de l’application est de 1.5ms.

TL;DR;

  • L’API Context n’est pas une solution magique et conduit rapidement à des problèmes de performances
  • Passer trop de props à des composants enfants peut permettre d’optimiser l’application mais la complexifie en même temps
  • Se concentrer sur le Context et mémoïser certaines méthodes permet d’optimiser l’ensemble de l’application sans toucher aux composants

Merci à toutes les personnes ayant relu cet article avant publication.

--

--