Contactez-nous

Mises à jour du contexte et optimisation (séparation des contextes)

Apprenez à optimiser les performances de l'API Context React en gérant les mises à jour de valeur et en séparant les contextes pour éviter les re-renders inutiles.

Le défi : Les re-renders liés au contexte

L'un des aspects fondamentaux de l'API Context est que lorsqu'un composant `Provider` reçoit une nouvelle valeur pour sa prop `value`, tous les composants descendants qui consomment ce contexte (via `useContext` ou ``) sont automatiquement re-rendus, même si la partie spécifique de la valeur du contexte qu'ils utilisent n'a pas changé.

Ce comportement est généralement souhaité, car il assure la synchronisation des données. Cependant, si la valeur fournie au `Provider` change trop fréquemment, ou si elle inclut des données non pertinentes pour certains consommateurs, cela peut entraîner des re-rendus inutiles et affecter les performances de l'application, en particulier si l'arbre de composants sous le `Provider` est grand ou si les consommateurs sont coûteux à rendre.

Il est donc crucial de comprendre comment les mises à jour du contexte déclenchent les re-rendus et d'appliquer des techniques d'optimisation pour minimiser les mises à jour superflues.

Optimisation 1 : Stabiliser la valeur du `Provider`

Le piège le plus courant est de passer un objet ou un tableau créé "à la volée" dans la prop `value` du `Provider`. Comme ces objets/tableaux obtiennent une nouvelle référence à chaque rendu du composant parent contenant le `Provider`, React considérera que la valeur du contexte a changé à chaque fois, même si le contenu interne est identique.

function ParentComponent() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: 'Alice' });

  // !!! MAUVAIS : Nouvel objet { theme, user } créé à chaque rendu de ParentComponent
  // Provoque le re-rendu de TOUS les consommateurs de AppContext à chaque fois
  // return (
  //   
  //     {/* ... enfants ... */}
  //   
  // );

  // --- BONNES Approches --- 

  // Approche A: Mémoriser l'objet valeur avec useMemo
  const contextValue = useMemo(() => ({ theme, user }), [theme, user]);

  // Approche B: Utiliser useState ou useReducer pour la valeur du contexte elle-même
  // (Déjà fait ici pour theme et user séparément)

  return (
    
      {/* ... enfants ... */}
    
  );
}

En utilisant `useMemo` (ou en s'assurant que la valeur provient directement de `useState` ou `useReducer`, qui garantissent des références stables tant que la valeur ne change pas réellement), vous vous assurez que la référence de l'objet `value` ne change que lorsque ses dépendances (`theme` ou `user` dans cet exemple) changent vraiment. Cela évite les re-rendus inutiles des consommateurs lorsque `ParentComponent` se re-rend pour une autre raison.

De même, si vous passez des fonctions dans la valeur du contexte (par exemple, des fonctions pour mettre à jour l'état), enveloppez-les avec `useCallback` pour garantir qu'elles conservent la même référence entre les rendus, sauf si leurs propres dépendances changent.

function AuthProvider({ children }) {
  const [token, setToken] = useState(null);

  const login = useCallback((newToken) => {
    setToken(newToken);
    // ... autres logiques de login
  }, []); // Dépendances de login (vides ici)

  const logout = useCallback(() => {
    setToken(null);
    // ... autres logiques de logout
  }, []); // Dépendances de logout

  // Mémorise la valeur complète du contexte
  const value = useMemo(() => ({ token, login, logout }), [token, login, logout]);

  return (
    
      {children}
    
  );
}

Optimisation 2 : Séparer les contextes

Une stratégie d'optimisation très efficace consiste à diviser un gros contexte contenant de nombreuses valeurs en plusieurs contextes plus petits et plus ciblés.

Imaginez un contexte `AppContext` contenant à la fois `theme` et `user`. Si seul `theme` change, tous les composants qui consomment `AppContext` seront re-rendus, même ceux qui n'utilisent que `user`. En séparant `theme` et `user` dans deux contextes distincts (`ThemeContext` et `UserContext`), un changement de `theme` ne provoquera que le re-rendu des composants consommant `ThemeContext`, et un changement de `user` ne provoquera que le re-rendu des composants consommant `UserContext`.

// AVANT : Un seul contexte large
// const AppContext = createContext();
// function App() {
//   const [theme, setTheme] = useState('light');
//   const [user, setUser] = useState({ name: 'Alice' });
//   const value = useMemo(() => ({ theme, user }), [theme, user]);
//   return (
//     
//        {/* Utilise theme */} 
//        {/* Utilise user */} 
//     
//   );
// } // Si theme change, ComponentA ET ComponentB re-rendent.

// APRES : Contextes séparés
const ThemeContext = createContext();
const UserContext = createContext();

function App() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: 'Alice' });
  
  const themeValue = useMemo(() => ({ theme }), [theme]);
  const userValue = useMemo(() => ({ user }), [user]);

  return (
    
      
         {/* Utilise useContext(ThemeContext) */} 
         {/* Utilise useContext(UserContext) */} 
      
    
  );
}
// Si theme change, seul ComponentA re-rend.
// Si user change, seul ComponentB re-rend.

Cette séparation permet de découpler les mises à jour. Un composant ne sera re-rendu que si le contexte spécifique qu'il consomme a effectivement changé. C'est souvent la meilleure approche pour les données qui évoluent indépendamment les unes des autres.

On peut pousser cette logique plus loin : si un contexte contient à la fois des données et des fonctions pour modifier ces données (par exemple, `{ state, dispatch }` issu de `useReducer`), et que certains composants n'ont besoin que des fonctions de modification (qui sont souvent stables grâce à `useCallback`), on peut envisager de créer deux contextes séparés : un pour l'état et un pour les fonctions de dispatch/mise à jour. Ainsi, les composants qui n'ont besoin que de déclencher des actions ne seront pas re-rendus lorsque l'état change.

Optimisation 3 : `React.memo` et props (moins lié au contexte mais utile)

Bien que cela ne soit pas spécifique à `useContext`, si un composant consommateur (`ComponentA` dans l'exemple précédent) est lui-même coûteux à rendre, et qu'il reçoit des props qui ne changent pas, vous pouvez l'envelopper dans `React.memo`. `React.memo` empêchera le re-rendu de `ComponentA` si ses propres props n'ont pas changé, même si son parent (qui contient les Providers) se re-rend.

Cependant, `React.memo` ne suffit PAS à empêcher le re-rendu si la valeur du contexte consommée par le composant change. L'optimisation doit d'abord se faire au niveau de la stabilité de la valeur du `Provider` et de la séparation des contextes.

Conclusion : Penser aux performances dès la conception

L'API Context est un outil puissant, mais comme tout outil puissant, il doit être utilisé judicieusement pour éviter les problèmes de performance. Le principal levier d'optimisation est de minimiser les re-rendus inutiles des composants consommateurs.

Les stratégies clés sont :

  • Stabiliser la prop `value` du `Provider` : Utilisez `useMemo` pour les objets/tableaux et `useCallback` pour les fonctions passées dans la valeur.
  • Séparer les contextes : Divisez les contextes larges en contextes plus petits et spécifiques pour découpler les mises à jour. Ne regroupez dans un même contexte que des données qui changent généralement ensemble ou qui sont logiquement très liées.

En appliquant ces principes, vous pouvez tirer parti de la commodité de l'API Context sans sacrifier les performances de votre application React.