Aller au contenu principal
Retour au blogue

Développement logiciel

Gérer l'état global React en 2022

Jean-Christophe Séguin Cabana
22 avr. 2022 ∙ 17 mins
coder en React

Un de mes amis qui est actuellement en train d'apprendre React, m'a demandé si ça valait toujours la peine d'apprendre Redux, étant donné que l'API Contexte native fournit également une solution pour gérer l'état global et partager des données entre les composants. C'est une bonne question et l’une de celle qui revient le plus souvent. J'aborderai plus tard comment ce dernier n'est en fait pas vraiment idéal pour les états. Il s'agit toujours d'avoir le bon outil pour le bon travail. Dans certains cas, Redux reviendrait à manger des céréales avec une pelle, mais dans d'autres, cela offrirait une excellente solution pour gérer quelques exigences à la fois : état, mise en cache côté client, débogage, etc. J'ai fini par partager avec mon ami quelle serait probablement ma solution préférée ces jours-ci (selon la nature de l'application bien sûr). Sa question m'a donné envie d'approfondir un peu le sujet!

Dans cet article de blogue, je vais définir et décomposer ce que nous entendons par « état global » dans Contexte de React. Je vais ensuite explorer certaines des solutions populaires qui sont à notre disposition en 2022 et poser des questions telles que : Est-ce la bonne bibliothèque pour mon cas d'utilisation ? Est-ce exagéré ? Se chevauche-t-il avec une autre bibliothèque installée que je n'exploite pas complètement ? Est-ce que ça se synergise bien ? Etc. Je serai plus intéressé à explorer les réflexions que ces solutions modernes engagent, plutôt que de présenter une grande liste d'outils et d'énumérer leurs avantages et leurs inconvénients. Mon objectif est de me poser les bonnes questions lorsqu'il sera temps de peser les options pour une solution d'état globale, et d'apprendre à connaître les bibliothèques populaires, nouvelles ou remaniées. J'espère que cela vous aidera à atteindre le même objectif.

Anatomie d'un état global dans React

Une façon de voir l'état global dans Contexte de React est essentiellement de le considérer comme une information persistante, centralisée et globalement accessible qui détermine comment certains composants qui s'en soucient seront rendus. Comme l'indique le premier principe de Redux, il représente la source de vérité de votre application. Il diffère de l'état local, qui vise à encapsuler le flux de données dans un composant. React propose des solutions assez simples pour gérer l'état local, mais ne propose rien sur la manière dont les données doivent être gérées à l'échelle globale, d'où les nombreuses solutions disponibles et les débats fréquents.

Afin de mieux saisir nos besoins exacts, je voulais explorer en plus amples détails l'état global. Après avoir lu quelques variantes de répartition, je me suis retrouvé avec ces deux catégories:

  • État client: toutes sortes de fonctionnalités client globales. Il peut s'agir, par exemple, d'un flux d'interface utilisateur complexe dans lequel vous souhaitez conserver des informations d'une étape à l'autre, ou de quelque chose de simple comme des schémas de couleurs en mode sombre/clair, ou des instances d'entité complexes affectant différents composants que nous souhaitons peut-être mettre à jour.
  • État du serveur : données asynchrones qui sont récupérées, mises en cache, synchronisées et mises à jour à partir de l'état du serveur via une API (REST ou GraphQL) ou des WebSockets.

La raison pour laquelle j'aime cette répartition est que, comme l'a souligné l'équipe React-Query, l'état du serveur est relativement différent de l'état du client :

Pour commencer, l'état du serveur :

  • Celui-ci est conservé à distance dans un emplacement que vous ne contrôlez pas ou ne possédez pas
  • API pour récupérer et mettre à jour
  • Impliquent une propriété partagée et peuvent être modifiées par d'autres personnes à votre insu
  • Peut potentiellement devenir « obsolète » dans vos applications si vous ne faites pas attention

Les données d'état du serveur présentent différents défis (mise en cache, pagination, chargement différé, connaissance lorsque les données sont obsolètes, etc.). Les développeurs de React ont pour la majorité choisi Redux pour gérer la plupart de ces cas et pour tout mettre dans un seul magasin. Mais, ces dernières années, nous avons vu apparaître des solutions plus spécialisées et adaptées comme React-Query, SWR ou RTK Query (développées par Redux Toolkit, pour ceux attachés à Redux) pour s'attaquer spécifiquement à l'état du serveur. Certaines de ces solutions peuvent même rendre inutile une bibliothèque d'état globale si votre application ne dispose pas de fonctionnalités client globales. Nous avons également vu des options minimalistes plus simples qui pourraient être mieux adaptées pour gérer uniquement l'état du client, comme Zustand ou Jotai.

Ce qui nous amène à notre première grande question : mon application a-t-elle des états complexes côté client, traite-t-elle principalement l'état du serveur ou nécessite-t-elle les deux ? Nous partirons du principe que nous avons affaire à une application de taille moyenne à grande qui nécessite les deux (comme pour la plupart des projets que nous traitons chez Osedea).

Rester avec Redux ?

Lors d'une entrevue informelle, lorsque le coauteur de Redux, Dan Abramov, s’est fait demander « quand devrais-je utiliser Redux ? », j'ai été un peu surpris, mais heureux d'entendre ce qu'il avait à dire. Hésitant au départ, sa première réponse laissait entendre que si c'est déjà utilisé par l'équipe, c'est une raison suffisante en soi. C'est un point valable surtout que dans une grande équipe avoir des normes peut offrir certains avantages (intégration plus facile, conventions de nommage testées et prouvées, implémentations réutilisables, etc.). Mais on peut se demander si la norme en place est toujours d'actualité et c'est ce que Dan aborde par la suite à propos de Redux.

Lorsqu'on lui a demandé ce qu'il utiliserait aujourd'hui s'il avait à démarrer un nouveau projet, il a répondu qu'il n'utiliserait probablement pas Redux, mais plutôt une bibliothèque plus spécialisée pour gérer l'état du serveur et la mise en cache (ce qui est souvent ce pour quoi les développeurs utilisent Redux), comme React-Query, Apollo ou Relay. Comme le souligne Dan, cela dépend bien sûr du cas d’utilisation. En ce qui concerne les états plus locaux ou côté client uniquement, il a déclaré qu'il le hissait au sommet, cote à cote avec quelque chose comme Contexte par exemple. Il est intéressant d'observer que nous avons à nouveau à nouveau cette répartition de l'état client/serveur comme mentionné plus haut.

Alors pourquoi Redux ? Eh bien, maintenant que Redux Toolkit propose RTK Query, spécialisé dans l'état du serveur, nous pouvons affirmer que nous avons une solution qui répond à tous nos besoins. Si vous êtes habitué à Redux Toolkit, que vous avez affaire à une API REST (nous verrons plus tard comment une application utilisant GraphQL bénéficierait d'un meilleur combo) et que vous souhaitez profiter pleinement de toutes les fonctionnalités, alors Redux est un très choix valable. Mais il existe un autre principe important dans la programmation pour lequel nous n'envisageons pas Redux comme premier choix: KISS (Keep It Simple Stupid). Redux Toolkit propose des abstractions/outils destinés à simplifier la mise en œuvre de Redux, mais même dans ce cas, il y a encore beaucoup de code passe-partout (« boilerplate code ») contrairement aux bibliothèques plus récentes.

Comparons Redux Toolkit avec une solution plus récente

À présent, vous devez vous dire : « Bel article de blogue, mais où sont les extraits de code, mon chum ? », alors allons-y. J'utiliserai Zustand pour cet exercice. Zustand prétend en effet être une solution de gestion d'état petite, rapide et en constante évolution utilisant des principes de flux simplifiés. Nous utiliserons l'exemple d'implémentation de base de la documentation de Zustand, mais commençons par Redux Toolkit :

import React from "react";
import ReactDOM from "react-dom";
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { Provider, useDispatch, useSelector } from "react-redux";

const initialState = {
  bears: 0,
};

const bearsSlice = createSlice({
  name: "bears",
  initialState,
  reducers: {
    increment: (state) => {
      state.bears += 1;
    },
    removeAll: (state) => {
      state.bears = 0;
    },
  },
});

const { increment, removeAll } = bearsSlice.actions;

const bearsReducer = bearsSlice.reducer;

const store = configureStore({
  reducer: {
    bears: bearsReducer,
  },
});

function BearCounter() {
  const bears = useSelector((state) => state.bears.bears);
  return <h1>{bears} around here ...</h1>;
}

function Controls() {
  const dispatch = useDispatch();
  const increasePopulation = () => dispatch(increment());
  return <button onClick={increasePopulation}>one up</button>;
}

function App() {
  return (
    <>
      <BearCounter />
      <Controls />
    </>
  );
}

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

Voyons maintenant ce que Zustand a à nous offrir :

import React from "react";
import ReactDOM from "react-dom";
import create from "zustand";

const useStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}));

function BearCounter() {
  const bears = useStore((state) => state.bears);
  return <h1>{bears} around here ...</h1>;
}

function Controls() {
  const increasePopulation = useStore((state) => state.increasePopulation);
  return <button onClick={increasePopulation}>one up</button>;
}

function App() {
  return (
    <>
      <BearCounter />
      <Controls />
    </>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

Nous pouvons remarquer que Zustand a réduit le code passe-partout au strict minimum et obtenu le même résultat avec la moitié des lignes de code. Dans une application normale, cet exemple serait bien sûr divisé en différents fichiers afin de le rendre plus organisé et évolutif, mais cela nous donne une petite idée des différences entre les deux.

De plus, Zustand propose des implémentations très simples. Par exemple, la mémorisation des sélecteurs :

const users = useStore(useCallback(state => state.users[id], [id]))

Ou la persistance:

import create from "zustand"
import { persist } from "zustand/middleware"

export const useStore = create(persist(
  (set, get) => ({
    bears: 0,
    increasePopulation: () => set({ bears: get().bears + 1 })
  }),
  {
    name: "bears-storage", // defaults to localStorage
  }
))

Zustand peut utiliser les outils de développement Redux. Il a une manière très simple de gérer les actions asynchrones. Vous pouvez facilement configurer des middlewares, les utiliser en dehors de React, et bien plus encore. Donc pour conclure, il semble cocher la plupart des cases fournies par Redux, mais avec une implémentation plus simple et plus intuitive.

Bien, nous avons donc un combo satisfaisant, utilisant une bibliothèque spécialisée dans l'état du serveur, disons React-Query, plus une bibliothèque simple d'état client comme Zustand. Mais pourquoi ne pas simplement utiliser Contexte pour l'état du client, comme suggéré par Dan Abramov, et éviter les tracas liés à l'installation et à la mise à jour d'une autre bibliothèque ?

Qu'en est-il de Contexte ?

Je ne veux pas entrer dans trop de détails sur ce sujet, car il a déjà été traité dans bon nombre d’articles qui ont approfondis le sujet, mais pour résumer, nous pouvons revenir à la définition provenant de l’équipe React : « Contexte fournit un moyen de transmettre des données via l'arborescence des composants sans avoir à transmettre manuellement les accessoires à tous les niveaux. » Il n'a jamais été conçu pour gérer quoi que ce soit, comme le dit Mark Erikson: «Contexte est une forme d'injection de dépendance. C'est un mécanisme de transport - il ne gère rien. Toute gestion d'état est effectuée par vous et votre propre code, généralement via useState/useReducer.»

C'est pourquoi, dans une bonne implémentation, vous verrez normalement quelques Contextes, et pas seulement un gros Contexte central. Cela signifie que Contexte va à l'encontre du principe de « source unique de vérité » que nous avons vu précédemment. Pourquoi ne pas le faire ? Parce que, comme le mentionne l'équipe React dans la section «Caveats»: « Contexte utilise l'identité de référence pour déterminer quand re-render, il y a des pièges qui pourraient déclencher des renders involontaires chez les consommateurs lorsque le parent d'un fournisseur re-rend ». La performance est un argument suffisant pour moi. Mais aussi, le fait que Contexte ne réponde pas aux critères de ce qui fait un outil de gestion d'état, comme l' explique Mark Erikson.

Contexte peut toujours être un bon complément à une bibliothèque d'état de serveur, si les informations côté client que nous voulons transmettre sont minimales, délimitées par des sections d'application et n'ont pas besoin d'être centralisées. De cette façon, nous nous assurons que les composants sont rendus uniquement sur les changements.

D'autres combos intéressants ?

Étant donné que nous parlons toujours d'une application de taille moyenne à grande qui nécessite à la fois des états client et serveur, nous avons vu comment il semble y avoir une tendance à les diviser en deux ensembles d'outils différents. Comment choisissons-nous laquelle des deux bibliothèques prendre ? Étant donné que les données d'état du serveur sont souvent ce qui « occupe le plus de place » dans l'état global, je commencerais par vérifier à quel type d'API de serveur nous avons affaire.

Avec GraphQL?

Même si en réalité GraphQL nécessite simplement un point de terminaison POST, les développeurs utilisent généralement une bibliothèque côté client GraphQL, pour avoir une implémentation propre des requêtes et des mutations. Parmi les plus populaires, nous avons Apollo Client, Relay et URQL. Les trois proposent des solutions de mise en cache, qui peuvent être mises à jour manuellement et qui sont accessibles dans l’ensemble de votre application. Nous pouvons affirmer que ces solutions de mise en cache ne sont pas toutes nécessairement simples à utiliser (n’est-ce pas Apollo Client !), mais elles couvrent la partie de l'état du serveur de cette manière.

Nous pouvons toujours décider de n'utiliser que les abstractions de requête/mutation, d'abandonner leur solution de mise en cache et d'utiliser, par exemple, Redux pour stocker et mettre en cache les données, mais cela manquerait quelque chose que nous avons déjà dans la boîte, lorsque nous installons un client GraphQL. Cela étant dit, je peux facilement imaginer qu'une équipe perde le contrôle et utilise un mélange de données centralisées/sélectionnées dans le cache du client Apollo, tandis que d'autres données récupérées sont stockées dans Redux, ce qui peut créer beaucoup de confusion et nous faire perdre notre source de vérité centralisée…

Mais si vous décidez d'emprunter cette voie, toutes les combinaisons sont valables : Relay + Zustand, URQL + Jotai, Apollo Client + Redux Toolkit même. L'essentiel ici c’est de se poser les bonnes questions. Comment ces options peuvent-elles fonctionner en synergie ? Comment éviter les chevauchements ? Comment indiquons-nous clairement dans notre application que les états du serveur et du client sont stockés différemment ? Et enfin, la bibliothèque que je choisis est-elle excessive pour l'état du client, dans un scénario où je n'ai besoin que de la partie des informations centralisées ?

Avec REPOS?

Les API REST étant toujours la norme populaire, elles s'adaptent très bien à toutes les solutions d'état globales générales que nous avons mentionnées jusqu'à présent. L'avantage d'utiliser une bibliothèque spécialisée pour l'état du serveur est de bénéficier d'abstractions qui facilitent la gestion du cache d'API que nous stockons côté client. Les trois choix populaires que j'entends souvent ces jours-ci sont RTK Query, React-Query et SWR. N'oubliez pas que RTK Query est un module complémentaire de Redux Toolkit et NON une solution autonome.

Avant de conclure…

Je n'ai évidemment pas mentionné toutes les solutions populaires qui nous sont proposées de nos jours. Je vais énumérer ceux que nous avons vus jusqu'à présent, ainsi que quelques autres options, au cas où vous souhaiteriez explorer davantage le sujet.

Pour l'état global général

À utiliser en complément d'une solution d'état de serveur, si nécessaire, ou à mettre en œuvre votre propre solution au sein de son écosystème:

  • Jotai - une « gestion d'état primitive et flexible pour React » qui utilise un modèle atomic.
  • MobX - « une bibliothèque qui rend la gestion d'état simple et évolutive en appliquant de manière transparente la programmation réactive fonctionnelle (TFRP). »
  • Recoil - une bibliothèque de gestion d'état à croissance rapide pour React, développée par l'équipe React.
  • Redux Toolkit - « conçu pour être le moyen standard d'écrire la logique Redux ».
  • XState - Une « Machines à états finis JavaScript et TypeScript, avec des diagrammes d'états pour le Web moderne. » Approche différente des autres avec des fonctionnalités variées, mais néanmoins intéressantes.
  • Zustand - Une petite solution de gestion d'état Bearbones rapide et évolutive utilisant des principes de flux simplifiés, avec une API confortable basée sur des « Hooks » qui n'est pas passe-partout ou opiniâtre.

Et pour l'état du serveur

  • Apollo Client - « une bibliothèque complète de gestion d'état pour JavaScript qui vous permet de gérer à la fois les données locales et distantes avec GraphQL. »
  • React-Query - « Performant et puissante synchronisation des données pour React. »
  • Relay - « un framework JavaScript pour récupérer et gérer les données GraphQL dans les applications React qui met l'accent sur la maintenabilité, la sécurité des types et les performances d'exécution. »
  • RTK Query - « ajout optionnel inclus dans le package Redux Toolkit », éliminant le besoin d'écrire à la main la récupération de données et la logique de mise en cache.
  • SWR - « une bibliothèque React Hooks pour la récupération de données »
  • URQL - « un client GraphQL hautement personnalisable et polyvalent pour React, Svelte, Vue ou JavaScript simple. »

Remarquez comment certains d'entre eux sont pour React seulement et d'autres sont plus agnostiques…

C'est un peu écrasant… Je ne sais toujours pas comment choisir

Nous avons discuté en détail de certains des principaux critères lorsqu'il est temps de choisir les bonnes bibliothèques d'états. Voici quelques-unes des questions importantes que nous avons vues jusqu'à présent :

  1. Ai-je besoin à la fois des états client et serveur ?
  2. Quelles sont les fonctionnalités exactes dont mon application a besoin pour l'état global ?
  3. Si j'ai principalement besoin de l'état du serveur, mes informations côté client sont-elles suffisamment simples et étendues pour utiliser un ou plusieurs Contextes pour les transmettre, ou ai-je besoin d'une solution plus centralisée et persistante (alias un état).
  4. Ai-je besoin d'une solution spécifique à GraphQL et couvre-t-elle déjà mes besoins en état de serveur ?
  5. Mes choix pour les états du serveur et du client ont-ils une bonne synergie ? Sont-ils optimaux ?
  6. Ces bibliothèques sont-elles faciles à implémenter et à maintenir?

On peut aller un peu plus loin en ajoutant une excellente liste des dix attributs de qualité proposés dans un article concernant la gestion des états React:

  1. Maniabilité
  2. Maintenabilité
  3. Performance
  4. Testabilité
  5. Évolutivité (fonctionne avec les mêmes performances sur les plus grands états)
  6. Modifiabilité
  7. Réutilisabilité
  8. Écosystème d'outils d'assistance pour étendre la fonctionnalité)
  9. Communauté (a beaucoup d'utilisateurs et leurs questions sont répondues sur le Web)
  10. Portabilité (peut être utilisé avec des bibliothèques/frameworks autres que React)

De cette façon, vous pouvez effectuer vos propres recherches et trouver la meilleure solution pour les exigences de votre application.

Puis-je choisir de ne pas prendre de bibliothèque ?

Un de mes collègues à Osedea, Zack, à récemment entamé un chemin bien différent et a complètement éliminé le besoin de bibliothèques d'états globales. Il partage son approche dans son article de blogue, où il propose une solution révolutionnaire utilisant les principes de conception pilotée par domaine et la programmation orientée objet, qui découple presque complètement l'état global de React, à l'exception de quelques adaptateurs simples.

Il est né d'un cas d'utilisation spécifique autour de diverses entités, contenant une logique métier lourde, qu'il fallait partager entre différents services, de manière agnostique. En bref, il n'y a jamais de solution unique et vous devez toujours prendre le temps de réfléchir aux exigences de votre application avant de choisir une solution.

Alors, qu'avez-vous dit à votre ami ?

Revenons à la question initiale de mon ami, qui était « est-ce que ça vaut toujours la peine d'apprendre Redux… » ? J'ai répondu que oui, je pense que ça vaut le coup, surtout pour un développeur en apprentissage. C'est encore souvent la norme dans l'industrie. Une fois que celui-ci à maîtrisé cela, il peut apprendre des solutions récentes, plus simples. Maintenant, qu'est-ce que j'utiliserais personnellement aujourd'hui, si je commençais un nouveau projet ? Cela n'a pas d'importance. Rien de tout cela n'a d'importance. Nous sommes tous des 1 et des 0 naviguant à grande vitesse dans une galaxie condamnée.

Sources:

Crédit Photo: Lautaro Andreani.