3 octobre 2022
Contextes et performances
4 minutes de lecture
Un peu de contexte
Gérer les données locales d’une application React a toujours été une question centrale d’architecture. Les premières applications propageaient simplement les données via les props (props drilling), polluant les composants servant d'intermédiaires. D'autres façons ont depuis vues le jour.
Redux
Redux est une librairie de gestion d'état (state manager). C'est en 2015, entre deux conférences de React Europe se déroulant à Paris, que Dan Abramov et Andrew Clark posent la première implémentation de la librairie.
Celle-ci permet de palier les problèmes de props drilling grâce à un workflow unidirectionnel et un state global que les composants consomment. Redux a été majoritairement adopté par la communauté React, apportant un pattern robuste pour développer de larges applications.
Cependant, l’utilisation de Redux dans une base de code est assez prolixe (beaucoup de code redondant), mais ce point a été corrigé grâce à des libraires telles que Redux Toolkit ou Rematch (dont nous avions parlé dans cet article).
L'API Context
Dans sa version 16.3, React introduit l’API Context, jusqu’ici expérimentale. Cette nouvelle API remplace en partie le rôle de Redux : offrir un state global que les composants peuvent consommer. L’introduction du hook useContext achève la démocratisation de l'API rendant son utilisation encore plus facile.
Beaucoup de personnes ont alors remplacé Redux par les contextes. Cependant, l'usage basique des contextes n'offre pas autant de fonctionnalités que Redux.
Parmi l'une d'elles : l'optimisation des performances.
Contextes et performances
Le problème
Dès qu'un composant utilise useContext()
, il écoute l'ensemble du contexte et pas uniquement les propriétés récupérées.
Dans l'exemple suivant, si l'utilisateur change le nom via setName
, alors le composant <DashboardWidgets />
sera, lui aussi, re-rendu même s'il n'utilise pas name
:
import { createContext } from 'react'
const AppContext = createContext()
const AppProvider = (props) => <AppContext.Provider {...props} />
const App = () => (
<AppProvider>
<DashboardHeader />
<DashboardWidgets />
</AppProvider>
)
const DashboardHeader = () => {
const { name, setName } = useContext(AppContext)
return (
<header>
<h1>Dashboard: {name}</h1>
<input value={name} onChange={(e) => setName(e.currentTarget.value)} />
</header>
)
}
// Ce composant sera re-render à chaque fois que l'utilisateur tape
// une lettre dans l'input du composant DashboardHeader
const DashboardWidgets = () => {
const { widgets } = useContext(AppContext)
return (
<div>
{widgets.map((widget) => (
<Widget data={widget} />
))}
</div>
)
}
Avec Redux, nous pouvons cibler uniquement widgets
et ignorer ainsi les mises à jour liées au nom :
import { useSelector } from 'react-redux'
const DashboardWidgets = () => {
const widgets = useSelector(state => state.dashboard.widgets)
return (
<div>
{widgets.map((widget) => (
<Widget data={widget} />
))}
</div>
)
}
Dès lors que vous insérerez un useContext
dans un composant, celui-ci va se re-rendre même s’il ne consomme rien. Sur de petites applications, cela ne pose pas de problèmes, mais peut en entraîner sur de plus larges applications (dans notre exemple, re-rendre les widgets peut être coûteux).
Nous sommes donc en droit de nous poser cette question : comment utiliser les contextes d'une manière efficiente ?
La solution
Elle vient de la librairie dai-shi/use-context-selector. Celle-ci permet tout simplement d'utiliser le même mécanisme que Redux, à savoir l'utilisation de sélecteurs !
Son API est très simple à utiliser :
Remplacez createContext
avec celui de la librairie use-context-selector :
// import { createContext } from "react"
import { createContext } from 'use-context-selector'
const AppContext = createContext()
const AppProvider = (props) => <SearchContext.Provider {...props} />
Remplacez useContext
par le useContextSelector
, cette fonction accepte désormais une fonction en deuxième argument (un sélecteur) :
import { useContextSelector } from 'use-context-selector';
const DashboardWidgets = () => {
// const { widgets } = useContext(AppContext)
const widgets = useContextSelector(AppContext, (state) => state.widgets)
return (
<div>
{widgets.map((widget) => (
<Widget data={widget} />
))}
</div>
)
}
En ciblant notre propriété grâce à notre sélecteur, nous évitons ainsi les re-renders inutiles !
Afin d'éviter de disperser vos sélecteurs dans votre code, vous pouvez créer un custom hook :
export const useWidgetsSelector = () => {
return useContextSelector(AppContext, (state) => state.widgets)
}
Pour finir
Nous avons vu la principale différence entre les contextes et Redux :
- avec les contextes, un composant écoute l’ensemble du state du contexte ;
- avec Redux, un composant écoute de manière sélective le state, en indiquant ce qu’il souhaite écouter.
Ce détail d'implémentation de l'API Context peut ainsi mener à des problèmes de performances. Un développeur peut utiliser un contexte dans un composant sans savoir qu'il va entrainer un rafraichissement de celui-ci. Utiliser la librairie dai-shi/use-context-selector permet alors de consommer les contextes d'une manière plus efficiente et éviter un leak de performance.
Pour de larges applications utilisant beaucoup de state client, la paire Redux + Rematch reste aussi un très bon choix !
👋