Loupe

Mettez en place une implémentation Redux like avec les hooks React et la context API !

Olà !

Récemment, j’ai mis en place dans un projet un système de state management en utilisant les hooks de React et je me suis dit que ça serait surement sympa de vous faire partager ça :) Pour en savoir plus sur les hooks, c'est par

Tout d’abord, quand on parle de state management en React, on a souvent très vite le mot Redux à la bouche. Cette librairie javascript permettant d’intégrer une architecture Flux au sein de nos projets s’est très vite imposée comme un incontournable d’une stack front et est devenue au fil du temps l’une des librairie React les plus téléchargées ! (3M de téléchargements hebdomadaires).

Redux, bien que très pratique, n’est pas souvent la meilleure solution quand on souhaite implémenter du state management. Sa complexité de setup et son côté très verbeux (reducer, action, tasks, connect) en font souvent une option fastidieuse et pas toujours très pertinente quand on est sur un projet de petite à moyenne taille. Mais malgré ces mauvais côtés, pendant longtemps les alternatives ont manqué et le choix de Redux s’imposait souvent comme une évidence…

Heureusement pour nous, React a mis en place au fil du temps des outils nous permettant aujourd’hui de nous passer de Redux lorsque ce n’est pas forcement nécessaire. Et c’est de deux outils que l’on va parler aujourd’hui : la Context API et les hooks React !

La Context API

La context API de React nous permet de créer un context dans notre application. Un context nous permet de définir des données globales que l’on peut exposer (provide) à un ensemble de composants. Ces composants peuvent ensuite récupérer les données du context et s’en servir si besoin. Le but principal de la Context API est de nous permettre de ne plus avoir à passer des données de parent à enfant via les props jusqu’aux composants qui souhaitent les utiliser.

Voyons donc un exemple d’utilisation d’initialisation de context :

export const AppContext = React.createContext('toto');

const App = () => {
    return (
	    <AppContext.Provider value="tutu">
		    <ChildrenComponent>
			    <SubChildrenComponent>
				    <AnotherSubChildrenComponent />
			    </SubChildrenComponent>
		    </ChildrenComponent>
	    </AppContext.Provider>
    );
}

Dans ce snippet, nous créons tout d’abord une variable AppContext qui contiendra les informations dont React a besoin pour identifier le context. Nous initialisons ensuite cette variable via React.createContext qui permet de créer un context en lui passant en paramètre la valeur initiale du context, à savoir une string avec la valeur toto.

Ensuite, pour que le context puisse être rendu disponible à l’ensemble de notre app, nous devons encapsuler les éléments enfants du context dans un composant qui va exposer le context et qui doit respecter la syntaxe suivante : <NomDeMonContext.Provider />.
La prop value, quant à elle, sert à lui assigner une valeur ; dans notre cas le context sera initialisé dès le début avec la valeur tutu.

Maintenant que nous avons vu comment exposer un context React, voyons comment récupérer sa valeur :

import { AppContext } from './index.js';

const AnotherSubChildrenComponent = () => { 
    return (
		<AppContext.Consumer>
			{(context_value) => <TotoComponent value={context_value} />}
		</AppContext.Consumer>
	);
}

Et c’est aussi simple que ça ! On import la référence de notre context pour ensuite récupérer dans notre template la valeur du context grâce au consumer que nous appelons avec la syntaxe suivante : <NomDeMonContext.Consumer/>.
Ce consumer nous permet ensuite de passer une fonction en children qui prend en paramètre la valeur du context que l’on peut ensuite passer en prop à un composant enfant.

Il y a également une autre façon de récupérer un context, via un Hook fournit par React, j’ai nommé useContext ! L’avantage de cette deuxième version par rapport à celle que l’on vient de voir est qu’elle nous évite de passer par le template pour récupérer le context, ce qui permet souvent de ne pas avoir un composant intermédiaire qui est juste là pour récupérer le context. 

Le hook s’utilise très simplement, comme ceci :

import React, { useContext } from 'react';
import { AppContext } from './index.js';

const AnotherSubChildrenComponent = () => {
    const context_value = useContext(AppContext);
    
    return (
		<TotoComponent value={context_value} />
	);
}

useReducer

Rappelons maintenant notre objectif : implémenter une version lite de Redux. A ce stade, via la context API, nous avons vu comment rendre disponible globalement dans une application React une valeur qui pourrait tout à fait être notre store. Nous avons également vu comment récupérer cette valeur. Il nous manque désormais la possibilité de modifier les valeurs de ce store afin de pouvoir exposer notre store mis à jour.

Afin que nos composants puissent mettre à jour le store (via des fonctions), il existe deux façons de faire :

  • Passer les fonctions de modifications de props en props à tous nos composants.
  • Passer les fonctions de modifications directement dans le context afin que les composants qui consomment le store récupèrent également les moyens de le mettre à jour.

Bien évidemment, la deuxième solution semble être la meilleure, car on n’a pas mis en place un context global dans notre application pour se retrouver à passer les fonctions de modifications de props en props.

Notre choix se porte donc sur la deuxième possibilité et pour cela, nous allons utiliser useReducer !

useReducer est un Hook également fournit par react, il nous permet d’implémenter un reducer pour modifier une valeur. Il fonctionne de manière similaire qu’un useState classique à l’exception que le setter normalement renvoyé par useState se fait ici via un dispatch d’une action comme dans un reducer Redux classique.

Les avantages de passer par un reducer sont multiples, il permet de garder le comportement d’une utilisation de Redux classique. Il permet également de mieux tracer les modifications du store en dispatchant des actions qui sont claires et définies à un seul endroit de notre application, à savoir : le reducer.

Ainsi pour utiliser useReducer :

import React, { useReducer } from 'react';

const initialValue = {
	items: []
};

const reducer = (state, action) => {
	switch(action.type) {
		case 'ADD_TASK':
			const newState = {...state}
			return  Object.assign(newState,  {items:  [...state.items, action.value]});
		case 'DELETE_LAST_TASK':
			const newState = {...state}
			return  Object.assign(newState,  {items:  state.items.slice(0, state.items.length - 1)});
		default:
			throw new Error();
	}
}

const AppComponent = () => {
	const [state, dispatch] = useReducer(reducer, initialValue);
	
	return <div/>;
}

Ainsi, dans cet exemple, et comme dans Redux, pour mettre à jour notre variable state, il suffira de dispatcher une action comme ceci :

dispatch({type: 'ADD_TASK', value: {checked: false, title: 'new task'}});

Context Api et useReducer ensemble

Maintenant que nous avons vu de manière indépendante la context API ainsi que useReducer, il ne nous reste plus qu’à mélanger les deux pour obtenir notre implémentation Redux-like !

Ainsi on obtiendra, côté composant parent :

import  React from  'react';

const initialStoreValue= {
	items: []
};

const reducer = (state, action) => {
	switch(action.type) {
		case 'ADD_TASK':
			const newState = {...state}
			return  Object.assign(newState,  {items:  [...state.items, action.value]});
		case 'DELETE_LAST_TASK':
			const newState = {...state}
			return  Object.assign(newState,  {items:  state.items.slice(0, state.items.length - 1)});
		default:
			throw new Error();
	}
}

export const AppContext = React.createContext([initialStoreValue, null]);

const AppComponent = () => {
	const contextValue = useReducer(reducer, initialStoreValue);

	return (
	    <AppContext.Provider value={contextValue}>
		    <ChildrenComponent>
			    <SubChildrenComponent>
				    <AnotherSubChildrenComponent />
			    </SubChildrenComponent>
		    </ChildrenComponent>
	    </AppContext.Provider>
    );
}

Dans cet exemple nous avons providé un context comme dans l’exemple de la contextAPI, la seule différence réside dans la valeur de notre context qui n’est plus qu’une simple valeur ou un simple objet mais bien la valeur retournée par notre useReducer, à savoir un tableau content notre context en premier élément et le dispatch en deuxième élément.

Et désormais, au niveau de notre composant enfant :

import React, { useContext } from 'react';
import { AppContext } from './index.js';

const AnotherSubChildrenComponent = () => {
    const [context_value, dispatch] = useContext(AppContext);
    
    const addNewTask = (task_title) => {
	    dispatch({type: 'ADD_TASK', value: {checked: false, title: 'task_title'}});
    }
    
    return (
		<input onClick={((e) => addNewTask(e.target.value))} />
	);
}

Ici, nous récupérons notre context avec useContext et nous le déstructurons pour en récupérer la valeur ainsi que le dispatch. Le composant possède ainsi la valeur du context ainsi que la possibilité de pouvoir le mettre à jour.
Je me permets d’ailleurs de spécifier ici quelque chose qui peut paraître évident sans quoi ce que l’on fait dans cet article ne fonctionnerait pas : Quand le context est mis à jour, chaque composant qui utilise useContext est re-render. Vous pouvez donc coupler tout cela à des useEffect pour ensuite déclencher des actions quand votre context est mis à jour !

Et voilà ! Nous avons désormais mis en place à la main une implémentation lite de Redux qui peut largement suffire à vos besoins lorsque vous avez besoin de state management global dans une application React de taille petite ou moyenne !

Happy Coding !

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus