Parmi les multiples frameworks JavaScript disponibles dans l'écosystème du développement web, React fait partie des solutions les plus adoptées aujourd'hui : elle a pour but de simplifier le développement d'interfaces visuelles grâce à sa philosophie d'utilisation de composants indépendants et réutilisables.
Chaque composant est isolé du reste de l'application et utilise deux types de données :
les props, des informations passées au composant par son composant parent
les state, des informations gérées localement par le composant
1. Problématique du prop-drilling
Lorsqu'une application React commence à se complexifier, il est récurrent de voir des cas où un développeur passera des props à un composant uniquement pour que celui-ci le transmette à ses composants enfants.
Ce cas ne pose généralement pas de problème lorsqu'il s'agit de passer une prop entre 2 ou 3 niveaux de composants.
Cependant, il arrive dans certains cas que l'information soit passée à plus de 5 niveaux différents dans l'arborescence des composants (par exemple, la gestion des données relatives à l'authentification).
On appelle cette problématique le prop-drilling, autrement dit, on passe des données à travers un grand nombre de composants alors qu'une minorité utilise l'information transmise. Afin de remédier à ce problème, une des solutions les plus utilisées est de mettre en place un state global, des informations accessibles à toute l'application. Cela peut être fait avec des solutions comme Redux ou la Context API de React.
2. Redux & Context API
Redux
Redux est une librairie externe à React qui se focalise sur la gestion de données dans des applications JavaScript. Il est important de noter que Redux peut également être utilisée avec d'autres applications web réalisées sans React, mais elle est régulièrement associée à ce framework.
Redux met à disposition un store, un objet JavaScript qui contient les différentes informations stockées globalement. La librairie react-redux met ensuite à disposition deux hooks très pratiques, useSelector et useDispatch, qui nous permettent de récupérer les données stockées globalement, mais aussi de déclencher des actions qui permettent de modifier ce state global.
Prérequis npm install --save redux react-redux
Ces 2 dépendances doivent être installées pour commencer à utiliser Redux sur une application React.
Mise en place du store Redux import { createStore } from "redux"; import { Provider } from "react-redux"; import { render } from "react-dom"; import reducer from "./reducer"; const store = createStore(reducer); const App = () => ( <Provider store={store}> <Page /> </Provider> ); render(<App />, document.getElementById("root"));
Cette première partie de code permet de mettre en place le store mis à disposition par Redux. Il correspond à l'objet qui contiendra les données "globales" partagées par le reste de l'application.
Mise en place du "reducer" de Redux const initialState = { isLogged: false }; export default function reducer(state = initialState, action) { switch (action.type) { case "SET_IS_LOGGED": return { ...state, isLogged: action.payload }; default: return state; } };
Cette fonction correspond au reducer utilisé par Redux. Dans cette fonction, on définit les données globales et les différentes actions qui permettent de modifier les données globales du store.
Un reducer reçoit 2 paramètres, le state global stocké actuellement dans Redux (où l'on précise le state initial) et ensuite les actions qui seront reçues par le reducer. Une action correspond à une "demande" de modification du state global, elle est représentée par un objet JavaScript comportant au moins une clé "type".
Le type de l'action comporte une chaîne de caractère unique qui sera utilisée pour identifier quelles sont les données à modifier dans le store de Redux.
Dans le bout de code ci-dessus, on utilise un switch permettant d'alterner le comportement du reducer en fonction du type de l'action.
Récupération des données globales et modifications des données import React from "react"; import { useSelector, useDispatch } from "react-redux"; export default function DeeplyNestedComponent() { const globalState = useSelector((state) => state); const dispatch = useDispatch(); return ( <div> {globalState.isLogged ? ( <button onClick={() => dispatch({ type: "SET_IS_LOGGED", payload: false })}> Log out </button> ) : ( <button onClick={() => dispatch({ type: "SET_IS_LOGGED", payload: true })}> Log in </button> )} </div> ); }
En utilisant les hooks useSelector et useDispatch, on peut récupérer les informations stockées globalement et changer dynamiquement l'affichage des boutons d'authentification. En "dispatchant une action", cela nous permet (dans notre exemple) de nous connecter ou de nous déconnecter.
useSelector : Hook permettant de récupérer les données qui sont stockées globalement, depuis n'importe quel composant
useDispatch : Hook permettant de récupérer une fonction "dispatch" qui permet d'envoyer des actions (sous forme d'objets) qui seront traitées par le reducer mis en place plus haut
Vous pouvez aller encore plus loin avec Redux en suivant la documentation disponible sur
leur site.
Context API
Apparue officiellement en Mars 2018, la Context API de React permet de stocker des informations dans un "contexte". Le contexte met à disposition un provider (un composant React) qui permet à tous les composants enfants d'accéder à l'information stockée dans le contexte. Son utilisation s'est notamment simplifiée avec l'arrivée du hook useContext mis à disposition nativement dans React.
Création du contexte import React, { useState, createContext } from "react"; const AuthContext = createContext(); const AuthProvider = (props) => { const [authState, setAuthState] = useState({ isLogged: false }); return ( <AuthContext.Provider value={[authState, setAuthState]}> {props.children} </AuthContext.Provider> ); }; export { AuthContext, AuthProvider };
Comme Redux, la Context API met à disposition un provider, le composant en charge de passer l'information stockée globalement. Cependant, contrairement à la notion de dispatch et actions de Redux, nous transmettons ici via le contexte un state et un setState. Dans ce cadre là, nous pouvons facilement récupérer et modifier l'information stockée globalement, comme si nous le faisions dans un composant local. La Context API ne restreint pas le type d'information qui peut être transféré. Vous pouvez très bien fournir une valeur (une chaîne de caractères, un nombre, une date, …), ou bien un objet ou un tableau d'éléments.
Mise en place du contexte import React from "react"; import { AuthProvider } from "./contexts/auth-context"; import AppRouter from "./navigation/app-router"; export default function App() { return ( <div> <AuthProvider> <AppRouter /> </AuthProvider> </div> ); }
Comme dans le cadre de Redux, nous plaçons le provider à la racine de l'application afin que l'information soit disponible partout. Il est intéressant de noter que cette décision est purement arbitraire : le store de Redux est généralement toujours placé à la racine de l'application, alors qu'un contexte peut très bien être positionné plus profondément dans l'arborescence de composants de l'application, on pourrait par exemple l'appliquer à un écran en particulier.
Récupération et modifications des données globales du contexte import React, { useContext } from "react"; import { AuthContext } from "./context"; function AuthButtons() { const [authState, setAuthState] = useContext(AuthContext); return ( <div> {authState.isLogged ? ( <button onClick={setAuthState((state) => ({ ...state, isLogged: false }))}> Log out </button> ) : ( <button onClick={setAuthState((state) => ({ ...state, isLogged: true }))}> Log in </button> )} </div> ); } export default AuthButtons;
L'usage du hook useContext nous permet de récupérer notre authState et notre setAuthState que nous avons passé dans le paramètre "value" de notre provider.
Pour plus d'informations sur la
Context API de React, vous pouvez vous rendre vers
sa documentation.
3. Comparaison
Redux et la Context API sont deux solutions viables afin de mettre à disposition un state global. Nous allons maintenant lister les points positifs et négatifs de chaque solution pour vous permettre de sélectionner la meilleure option.
Redux
Avantages :
Architecture des responsabilités (reducers, actions, …) bien définie et constante
Des extensions de navigateur comme "Redux DevTools" sont disponibles et permettent de facilement débugger l'intégration de Redux dans un projet utilisant React
Inconvénients :
Difficile à adopter lorsqu'on débute sur React
Librairie externe à React : les deux packages nécessaires (redux & react-redux) représentent des données supplémentaires à télécharger pour l'utilisateur final
Context API
Avantages :
Solution native à la librairie React, elle est disponible dès que l'on commence une nouvelle application React
Prise en main très facile
Il est possible d'intégrer des contextes plus "locaux", c'est-à-dire qui n'englobent pas l'intégralité de l'application mais plutôt une petite quantité de composants : cela permet de segmenter la logique de l'application
Inconvénients :
L'API Context n'est pas adaptée pour des changements fréquents de state (par exemple, pour la valeur d'un champ de texte)
Tous les composants consommant le contexte vont être re-render à chaque changement du state global proposé par le contexte, même si l'information qu'ils utilisent n'est pas changée
Conclusion
Redux est une librairie utilisée depuis 2015, elle est présente dans une très grande quantité d'applications React en production. Elle a été testée et approuvée à de nombreuses reprises et restera une option particulièrement intéressante lorsqu'une application React commence à prendre de l'ampleur et à avoir une base de code conséquente.
Cependant, si vous souhaitez intégrer quelques données globales rapidement et d'une manière plus simplifiée, la Context API semble pouvoir répondre à vos attentes. Elle est intégrée directement dans React.
Aucune de ces deux options n'est prédominante. En fonction de la portée de votre projet et de l'implémentation que vous souhaitez réaliser, une de ces 2 solutions vous permettra de gérer les données globales de votre application.
Pour aller plus loin, je vous propose les liens suivants qui proposent des informations additionnelles qui pourraient orienter plus précisément votre choix :