React.js #3 : Gérer vos données dans des stores natifs

Cet article est le dernier article d'une série de trois articles dans laquelle nous voyons comment bien débuter son projet React.

Retrouvez tous les articles ici :

Au cours de ce dernier article nous allons aborder le sujet de la gestion des stores dans React.js.

Un store permet de stocker des données de manière centralisée et partagées entre tous les composants de notre application.

Auparavant, les dépendances tierces comme Flux, Redux ou Mobx permettaient de gérer les données et coordonner les composants de nos applications complexes. Désormais, Facebook a intégré les Context et les Hooks qui permettent d'avoir un système de store natif. Si vous n’êtes pas à l’aise avec ce concept de store, nous allons l'aborder plus concrètement dans la suite de l'article.

Dans notre architecture, les stores de nos applications sont des services. Nous y avons défini un premier store themeStore.js qui nous servira d’exemple tout au long de cet article. Pour gérer le store nous utiliserons les fonctionnalités natives à React.js (depuis la version 16.8) : Context et Hooks. Ainsi nous ne parlerons pas de Redux ou encore de Mobx qui sont d’autres solutions de gestion du store pour React.js.

Rapide point sur l’avis de React.js sur comment gérer son store

A la question “Devenons nous utiliser une librairie de gestion d’état comme Redux ou Mobx ?”, la documentation nous redirige vers la réponse à “Faut-il utiliser Redux” sur le site de Redux. La réponse est “non” dans un premier temps et "oui" si le besoin d’une source de vérité unique (ie. un état global à notre application) se faire ressenti. Mais cela ajoute de la complexité à notre application.

Récemment, nous n’avons plus vraiment besoin de Redux grâce à l’ajout de nouvelles fonctionnalités dans React que nous verrons plus tard. Faisons d’abord un rappel sur le state et les props en React.

Rappel de concepts importants dans React.js : composant, props et state

Un composant peut s’écrire de deux façons différentes :

  • Soit sous forme d’une fonction :
function Welcome(props) {
   return <h1>Hello, {props.name}</h1>;
}
  • Soit sous forme d’une classe :
class Welcome extends React.Component {
   render() {
       return <h1>Hello, {this.props.name}</h1>;
   }
}

Les deux écritures sont équivalentes.

Dans le développement de React.js, il semblerait que l’écriture sous forme de fonction soit de plus en plus privilégiée sans pour autant abandonner celle sous forme de classe. En effet, l’équipe React rassure en affirmant que les composants classe seront toujours maintenus dans un futur proche.

Il est donc fortement recommandé de désormais écrire tous vos composants sous forme de fonction si vous êtes à minima sous la version 16.8 de React.js

Pour ce qui est des props, on peut voir ça comme les paramètres d’entrée à notre composant. Ainsi quand on écrira notre composant Welcome, de l’exemple précédent, on devra lui passer un attribut name.

La principale différence qui existait entre l’écriture de composant sous forme de fonction et de classe était la gestion de l’état (state) du composant et du cycle de vie d’un composant. Mais avec l’apparition des Hooks, l’écriture de composant sous forme de fonction apporte des fonctionnalités équivalentes (ou presque) au composant sous forme de classe. En tout cas, l’ambition de React.js est de couvrir toutes les fonctionnalités des classes pour les composants fonction. Nous vous présenterons plus en détails les Hooks pour la gestion du store mais je vous invite à vous renseigner pour le hook qui permet de bénéficier d'un état local dans une fonction composant et le hook qui permet d'exécuter des effets de bord dans une fonction composant si ça vous intéresse. 🤩

NB : peu importe l’écriture que vous choisirez, sachez qu’il existe une équivalence.

Pourquoi un gestionnaire de store ?

Prenons l’exemple de Redux qui, malgré qu’il ne soit plus nécessaire, permet d’apprendre de bons principes d’architecture du code.

store_comparison_without_with_redux

A gauche sans Redux (ou tout autre gestionnaire de store), lorsqu’un composant est mis à jour, un état (state) global à l’application doit être stocké dans le composant racine donc une mise à jour doit remonter jusqu’à la racine puis redescendre dans tous les composants qui ont besoin de cette information mise à jour. Ce qui est assez laborieux et coûteux car les composants intermédiaires par lesquels passent l'état n'en ont pas nécessairement besoin.

Avec Redux, on a un store qui est séparé du reste de l'application et qui peut être mis à jour depuis n’importe quel composant et qui sera mis à jour pour tous les composants qui sont abonnés au store.

Le store permet de simplifier les changements de données au sein de l’application en centralisant tout à un unique endroit.

Présentation de Context

Dans une application React, les données sont normalement passées de haut en bas (du parent aux enfants) mais grâce aux Context, il est possible de fonctionner différemment. Les Context permettent de partager des données entre les composants sans passer par les props explicitement.

store_with_redux

Dans l’image ci-dessus, les Context correspondent à la logique symbolisée par les flèches bleues. D'une part avec le Provider qui permet d'injecter le store dans l'application et d'autre part avec le Consumer qui permet de lire les données dans le store. Cela permet de “s’abonner” au store et donc de savoir quand une donnée est modifiée dans celui-ci. Une autre façon de consommer le store et via les Hooks comme nous allons le voir au prochain point.

Présentation de Hooks

Comme nous l’avons dit plus haut, les Hooks arrivent dans une logique où l’équipe de développement de React.js souhaite que les composants fonction aient toutes les fonctionnalités des composants classe. Dans cette optique, nous privilégierons les composants écrits sous forme de fonction car pour utiliser les Hooks nous devons être dans un composant fonction.

store_with_redux

Les flèches vertes de notre image ci-dessus correspondent à l'écriture dans le store. Tout comme pour la partie lecture du store, elle peut s'effectuer de deux façons. Soit avec les Hooks et plus particulièrement le Hook useContext ou avec le Context.Consumer qui retournent tous les deux une méthode dispatch qui permet de modifier l'état de notre store.

Un cas concret

Comme spécifié dans l’introduction, nous allons expliquer le fonctionnement de notre store avec le fichier themeStore.js. Voyons à quoi ressemble notre store, comment on peut lire les données qui s’y trouvent et enfin comment on peut modifier notre store.

1. Définition du store

Pour définir notre store nous allons donc utiliser les deux concepts vus précédemment, à savoir, les Context et les Hooks. Notre store se découpe en 3 parties.

La première partie est l’état initial de notre store, c’est un simple objet javascript :

const initialState = {
    theme: 'green',
};

Dans notre cas on souhaite partager à toute notre application le thème de notre application qui est une couleur, ici, le vert, par défaut.

La seconde partie est un reducer, plus simplement, une méthode qui va modifier l’état de notre application selon une action. Par exemple, nous allons définir une action changeTheme qui permettra de modifier la couleur du thème de notre application. Voyons à quoi ressemble cette méthode concrètement :

const reducer = (state, action) => {
   switch (action.type) {
       case 'changeTheme':
       return {
           ...state,
           theme: action.newTheme
       };
       default:
       return state;
   }
};

La troisième et dernière partie de notre fichier est la création d’un Context autour de notre store. On a besoin d’un Consumer qui permet aux composants de lire le store et de connaître son état en temps réel mais également de modifier son état. On a également besoin d’un Provider qui permet de “brancher” notre store à l’application.

const ThemeContext = createContext();

export const ThemeConsumer = ThemeContext.Consumer;
export const ThemeConsumerHook = () => useContext(ThemeContext);

export const ThemeProvider = ({children}) => (
   <ThemeContext.Provider value={useReducer(reducer, initialState)}>
       {children}
   </ThemeContext.Provider>
);

On commence par créer un Context que nous nommons ThemeContext. Nous exportons le Consumer de ce Context ainsi qu’un ThemeConsumerHook qui est le Hook React useContext. Ce sont deux façons de consommer et modifier l’état de notre store : soit par l’objet Consumer (pour les composants classe ou fonction), soit par le Hook (pour les composants fonction).

Enfin, nous exportons le Provider qui prend en paramètre un composant et qui le retourne entouré du Context Provider. Ceci permet au composant passé en paramètre de pouvoir accéder à ce store grâce au Hook useReducer qui prend en paramètre un reducer et l’état initial de notre store.

Au final, voici notre fichier themeStore.js :

import React, {createContext, useContext, useReducer} from 'react';

const initialState = {
    theme: 'green',
};

const reducer = (state, action) => {
   switch (action.type) {
       case 'changeTheme':
       return {
           ...state,
           theme: action.newTheme
       };
       default:
       return state;
   }
};

const ThemeContext = createContext();

export const ThemeConsumer = ThemeContext.Consumer;
export const ThemeConsumerHook = () => useContext(ThemeContext);

export const ThemeProvider = ({children}) => (
   <ThemeContext.Provider value={useReducer(reducer, initialState)}>
       {children}
   </ThemeContext.Provider>
);

2. Lecture du store dans les composants

Cette étape est elle-même composée de 2 sous-étapes :

  • utilisation du Provider de notre Contextpour “brancher” le store à notre application
  • utilisation du Consumer ou de la Consumer Hook pour lire le store

Premièrement, pour “brancher” notre store à notre application, il suffit d’entourer la balise de notre composant avec le Provider de notre store. Voici comment nous l’avons fait dans notre fichier App.js :

 <ThemeProvider>
     <Router>
       <Switch>
         {
           getRoutes().map((route, index) => {
             return <MyRoute exact {...route} key={index} />
           })
         }
         <Route component={NotFound} />
       </Switch>
     </Router>
   </ThemeProvider>

Pour revoir les explications sur le routing je vous invite à vous référer à l’article 2 de cette série. 😉

Notre composant ThemeProvider encapsule toute notre application. Nous l’importons depuis notre store tout simplement comme ceci :

import { ThemeProvider } from 'stores/themeStore';

(Pour tout savoir sur comment mettre en place les imports absolus, rendez-vous dans l’article 1 de cette série.)

Deuxièmement, pour consommer les données de notre store dans un composant englobé par le ThemeProvider, nous pouvons utiliser le ThemeConsumer pour les composants classe (ou fonction) ou bien le ThemeConsumerHook pour les composants fonction.

Voyons comment cela se présente avec un exemple :

const [{theme}, dispatch] : any = ThemeConsumerHook();

On appelle tout simplement le Hook ThemeConsumerHook qui provient de notre store. Celle-ci nous retourne deux paramètres avec en premier lieu l’état et en second lieu une méthode dispatch qui nous permet de mettre à jour le store. En utilisant l’affectation par décomposition d’un tableau et d’un objet on obtient directement la veleur de theme et la méthode dispatch...

Nous ne couvrirons pas la partie qui utilise ThemeConsumer car dans notre application nous utiliserons majoritairement le ThemeConsumerHook mais pour en savoir plus à ce sujet, vous pouvez vous rapprocher de la documentation de React.js à propos des Context.Consumer.

3. Ecriture dans le store depuis les composants

Concentrons nous également sur le cas avec le ThemeConsumerHook car c'est le plus courant dans notre cas mais il est tout à fait possible de faire la même chose avec le ThemeConsumer. Car ce sont deux façons de récupérer la méthode dispatch évoquée précédemment. Celle-ci permet de mettre à jour le store depuis un composant. Voyons comment nous pouvons l’utiliser avec cet exemple :

dispatch({
    type: 'changeTheme',
    newTheme: 'blue'
});

Nous passons un objet en paramètre de la méthode dispatch avec deux clés : type et newTheme. Il s’agit en fait d’un objet action. La première clé correspond au type de l’action. La deuxième clé newTheme est la nouvelle couleur du thème de notre application. Rappelez-vous, c’est au niveau du reducer que nous gérons les actions. C’est donc ici que nous définissons les actions possibles.

Pour conclure, voici un exemple de composant complet qui affiche un bouton au couleur du thème par défaut (vert) et qui après un click change de couleur (bleu), tout ça via le store :

import React from 'react';
import { ThemeConsumerHook } from 'stores/themeStore';

const Admin = () => {
   const [{theme}, dispatch] = ThemeConsumerHook();

   function handleClick() {
       dispatch({
           type: 'changeTheme',
           newTheme: 'blue'
       });
   }

   return (
       <>
           <h1>ADMIN</h1>
           <button
               style={{color: `${theme}`}}
               onClick={handleClick}
           >
               Make me blue!
           </button>
       </>
   );
}

export default Admin;

Merci d'avoir lu ce dernier article et suivi toute la série dédiée à l'initialisation d'un nouveau projet React.js scalable et organisé ! 😃

Tags de
l'article

React.JS Front-end

Catégories de l'article

Javascript

Commentaires

Il n'y a actuellement aucun commentaire. Soyez le premier !

Articles liés