L'agence
WanadevStudio
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 :
- React.js #1 : Comment architecturer son projet React.js ?
- React.js #2 : Comment gérer le routing et la sécurité sur une application React ?
- React.js #3 : Gérer vos données dans des stores natifs
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.
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.
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 est 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.
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 notreContext
pour “brancher” le store à notre application - utilisation du
Consumer
ou de laConsumer 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é ! 😃
Commentaires
Gilles
Il y a 3 ans
Hello,
Petite coquille "est" à la place de "et" dans la phrase : "' Une autre façon de consommer le store et via les Hooks comme nous allons le voir au prochain point."
Merci pour l'article si non :)
Louis GOURAIN
Il y a 3 ans