Si vous êtes développeur·se web, il y a de grandes chances pour que vous connaissiez déjà redux, au moins de nom. Et si ça ne vous dit rien, redux est une bibliothèque JavaScript de gestion d'état, permettant de stocker et de faire évoluer un ensemble de données, souvent représentant l'état interne d'une application web.

Le principe fondamental n'est pas bien compliqué :

  1. On a des données à un instant T
  2. Il se passe un événement qui doit modifier ces données
  3. Cet événement est traité par une fonction chargée de calculer les données à l'instant T + 1

Dans le cas d'une app web comportant un ensemble de règles métier complexes, ce mécanisme permet d'organiser, et donc de simplifier, les modifications de l'état interne de l'app.

Cet article est séparé en trois parties : quels sont les avantages et inconvénients qu'apporte une bibliothèque de gestion d'état, deep dive dans la mise en place de redux avec des explications et des exemples concrets, et enfin, comment lier le store à une app react.

Pourquoi utiliser un store manager ?

C'est une bonne question ça, pourquoi avoir besoin d'une bibliothèque comme redux dans le contexte d'une app web, alors que les frameworks frontend actuels permettent déjà de stocker et de manipuler de la donnée ?

Par exemple, react nous donne l'API useState (ou this.setState dans le cas de class component), vue nous donne la propriété data, et svelte permet simplement de déclarer des variables locales qui garderont leur valeur lorsque l'app est mise à jour.

Un des principaux avantages d'une bibliothèque de gestion d'état est de pouvoir centraliser l'information.

Prenons un exemple : imaginez que vous développiez un réseau social, avec un header affichant le nombre de conversations ouvertes. Si l'utilisateur ferme une conversation, il faudra décrémenter le nombre de conversations dans le header, ce qui peut être difficile car plusieurs parties de l'app complètement différentes partagent la même information.

Un gestionnaire d'état permet de résoudre ce problème, en étant l'unique source de vérité de l'app. Dans notre exemple, le store stocke la liste des conversations ouvertes, et la rend disponible à tous les composants.

On peut donc voir le store comme une base de donnée, qui est disponible tant que l'app est affichée par un navigateur. Modifier ces données pourra impacter plusieurs parties de l'app simultanément.

Un second avantage d'une bibliothèque de gestion d'état est l'application des principes de la clean architecture, c'est à dire pouvoir tracer une séparation claire entre d'un côté les données et la manière dont elles évoluent (les règles métier), et d'un autre la manière dont elles sont affichées ou utilisées, via un framework front.

Le fait de découpler le code domaine (métier) du framework frontend apporte beaucoup d'avantages, surtout sur une code base dont les règles métiers sont complexes. C'est un vaste sujet, qui sera expliqué plus en détail dans un autre article à propos de la clean architecture côté frontend (coming soon !).

Redux apporte une spécificité importante par rapport à d'autres store managers : il est conçu autour de la notion d'immutabilité. Le seul moyen de modifier l'état du store est de passer par un reducer, une fonction chargée de produire le nouvel état à partir de l'état actuel et d'une action (c'est le principe de la fonction reduce des tableaux en JavaScript).

De plus, ce reducer doit être une fonction pure, ce qui veut dire qu'un ensemble d'actions jouées dans le même ordre produiront toujours le même résultat. Et c'est bien pratique pour comprendre la plupart des problèmes d'une app disposant un état interne : la mutation des données.

Pour résumer, redux permet de stocker l'état abstrait d'une application web, et de le faire évoluer selon des règles métier aussi complexes que nécessaire, tout en le gardant découplé de la manière dont il est affiché et utilisé.

Plus de détails sur les avantages qu'apporte redux sont expliqués dans sa documentation.


Ce ne serait pas très fair play de ne parler que des avantages, alors que redux vient également avec son lot d'inconvénients.

Le principal (selon moi) étant qu'il est facile d'en faire une mauvaise utilisation, de complexifier le code sans pour autant résoudre de problèmes. C'est ce qui peut arriver lorsque redux est mis en place par des développeurs peu expérimentés, ou qui ont des deadlines ne permettant pas de s'assurer d'avoir bien compris comment l'utiliser correctement. Et comme les données sont au centre d'une app web, cela peut avoir un impact important sur la dette d'un projet.

Il faut aussi prendre en compte que de mettre en place un store redux implique de le maintenir, et donc de devoir prendre le temps de le refactorer s'il commence à poser plus de problèmes qu'il n'en résout. Mais globalement, le temps passé à maintenir le store est négligeable par rapport aux bénéfices qu'il apporte.

Un autre point d'attention à garder en tête est que comprendre redux n'a rien d'évident (et tout le tooling qui va autour, en particulier redux-thunk et redux toolkit). Apprendre à bien utiliser redux demande du temps, de la réflexion et de l'expérience.

Donc en définitive, l'utilisation d'une bibliothèque permettant de structurer la gestion de l'état interne d'un système apporte des avantages incontestable si ce système doit manipuler un jeu de données complexe, et plus particulièrement lorsque ces données répondent à des use case métiers non triviaux.

Let's dive in.

Mise en place de redux

Dans cette partie, nous allons entrer en détail dans le code nécessaire pour utiliser redux, à savoir, les reducers, les action creators et les selectors.

N'hésitez pas à vous référer à la doc de redux à tout moment, elle est vraiment bien faite.

On va commencer en douceur, avec le sempiternel exemple présenté dans la documentation de redux : un compteur. Notre état sera donc pour l'instant un simple nombre entier, que l'on pourra incrémenter.

Reprenons de manière un peu plus fine le cycle de vie d'un store redux :

  1. On initialise le store à partir d'un état initial
  2. On peut accéder aux données du store en lecture via des selectors
  3. Pour faire évoluer le store, on dispatch une action
  4. Un reducer permet de produire le nouvel état en fonction de l'état actuel et de l'action dispatchée
  5. L'état actuel est remplacé par la sortie du reducer et on revient au point 2
Schéma (simplifié) du fonctionnement de redux

Les reducers

On va commencer par le cœur de notre gestion d'état : le reducer. Un reducer est donc une fonction qui prend deux paramètres : l'état actuel (dans notre cas, un entier), et l'action qui décrit quel événement s'est produit. Le rôle du reducer est de savoir quelle mutation appliquer en fonction de l'action, en l'occurrence incrémenter le compteur.

Redux nous impose la structure d'une action : ce sera toujours un objet ayant au moins une clé type, dont la valeur est une string. Dans notre cas, on considérera l'action { type: "increment" }.

import { AnyAction } from 'redux';
 
const reducer = (state = 0, action: AnyAction): number => {
  if (action.type === 'increment') {
    return state + 1;
  }
 
  return state;
};

Exemple simple, difficile de faire plus simple même. Mettons tout de suite en application ce reducer, en créant un store via la fonction createStore exportée par redux. Une fois crée, nous pouvons accéder à son état actuel via sa fonction getState, et dispatcher une action via sa fonction dispatch, ce qui invoquera automatiquement notre reducer.

import { createStore } from 'redux';
 
const store = createStore(reducer);
 
// read the state
console.log(store.getState()); // 0
 
// update the state
store.dispatch({ type: 'increment' });
 
// read the state
console.log(store.getState()); // 1

Note : une action ne spécifie pas comment faire évoluer le store, elle ne fait que décrire un événement. C'est au reducer de savoir quelles mutations appliquer lorsque cet événement se produit. C'est assez proche du design pattern pubsub : dispatcher une action revient à publier un message qui peut être écouté par les reducers.

Les action creators

Plutôt que déclarer l'action directement dans l'appel à dispatch, nous allons passer par un action creator, une fonction avec un nom parlant, qui retourne l'action.

const increment = () => ({
  type: 'increment',
});
 
// ...
 
store.dispatch(increment());

Pendant qu'on y est, on va ajouter la possibilité d'incrémenter notre compteur d'une valeur custom. Easy, il suffit d'ajouter un nouvel action creator qui aura un payload, et ajouter une condition dans notre reducer pour traiter cette action.

const incrementByAmount = (amount: number) => ({
  type: 'incrementByAmount',
  amount,
});
 
const reducer = (state = 0, action: AnyAction): number => {
  // ...
 
  if (action.type === 'incrementByAmount') {
    return state + action.amount;
  }
 
  return state;
};
 
store.dispatch(incrementByAmount(5));

Penchons-nous maintenant du côté des types : l'action donnée en second paramètre de notre reducer est de type AnyAction, c'est un type exporté par redux, qui définit un objet contenant au moins la clé type, et n'importe quoi d'autre si on veut. Dans notre exemple, cela nous permet d'accéder à action.amount dans le reducer sans se faire crier dessus par TypeScript.

Mais cet accès n'est pas type safe. Nous, on voudrait que "si le type de l'action est exactement "incrementByAmount", alors l'action ne contient que le champ amount de type number en plus du champ type. Let's do this!

type IncrementAction = {
  type: 'increment';
};
 
const increment = (): IncrementAction => ({
  type: 'increment',
});
 
type IncrementByAmountAction = {
  type: 'incrementByAmount';
  amount: number;
};
 
const incrementByAmount = (amount: number): IncrementByAmountAction => ({
  type: 'incrementByAmount',
  amount,
});
 
type Action = IncrementAction | IncrementByAmountAction;
 
// note that we replaced AnyAction with Action right there
const reducer = (state = 0, action: Action): number => {
  if (action.type === 'increment') {
    return state + 1;
  }
 
  if (action.type === 'incrementByAmount') {
    // now, the action is correctly typed!
    return state + action.amount;
  }
 
  return state;
};

Dans cet exemple, nous utilisons le champ type de l'action comme discriminant pour connaître le type exact de l'action, comme expliqué dans la partie discriminated unions de la doc de TypeScript.

C'est mieux, mais c'est verbeux. Plutôt que d'expliciter le type de chaque action, on peut laisser TypeScript les inférer, via l'utilisation des types génériques (il y aurait de meilleures façons de faire, mais j'ai préféré garder les choses simples). Si vous n'avez pas fait TS generics LV3, acceptez ce code tel quel, c'est pas bien grave.

type Action<Type extends string, Payload> = { type: Type } & Payload;
 
function createAction<Type extends string, Payload>(type: Type, payload?: Payload): Action<Type, Payload> {
  return { type, ...payload } as Action<Type, Payload>;
}

Plus qu'à utiliser cette petite fonction dans nos action creators, et ~~PAF ! ça fait des chocapics~~ on peut se passer des déclarations de type de nos actions.

const increment = () => {
  return createAction('increment');
};
 
const incrementByAmount = (amount: number) => {
  return createAction('incrementByAmount', { amount });
};
 
// actions types will be inferred based on our action creators' returned values
type Action = ReturnType<typeof increment | typeof incrementByAmount>;

Ça va, vous suivez ? On enchaîne.

Combiner plusieurs states

Un compteur c'est bien, c'est simple, mais ça reste assez limité. Je propose qu'on ajoute une state un peu plus fournie : une question, constituée d'un texte et d'un ensemble de réponses, chaque réponse étant constituée d'un texte et d'un flag indiquant si elle est correcte.

type Question = {
  text: string;
  answers: Answer[];
};
 
type Answer = {
  text: string;
  correct: boolean;
};

On peut déclarer quelques action creators qui permettront de "set la question", "changer le texte de la question" et "ajouter une réponse".

const setQuestion = (question: Question) => {
  return createAction('setQuestion', { question });
};
 
const setQuestionText = (text: string) => {
  return createAction('setQuestionText', { text });
};
 
const addAnswer = (answer: Answer) => {
  return createAction('addAnswer', { answer });
};
 
type QuestionAction = ReturnType<typeof setQuestion | typeof setQuestionText | typeof addAnswer>;

Rien de nouveau en principe. Passons au reducer chargé de modifier l'état de notre store en fonction de ces actions. Par défaut, notre store sera vide, cette absence de question étant représentée par la valeur null. Mais que se passe-t-il si on dispatch l'action pour changer le texte de la question lorsque la valeur est null ? Meh, dans ce cas on va dire que l'action n'a pas d'effet, et voilà.

const questionReducer = (state: Question | null = null, action: Action): Question | null => {
  if (action.type === 'setQuestion') {
    return action.question;
  }
 
  // if no question is set, the other actions won't do anything
  if (!state) {
    return null;
  }
 
  if (action.type === 'setQuestionText') {
    return {
      ...state,
      text: action.text,
    };
  }
 
  if (action.type === 'addAnswer') {
    return {
      ...state,
      answers: [...state.answers, action.answer],
    };
  }
 
  return state;
};

Maintenant, nous pouvons ajouter cet état à notre store, à côté du compteur, en combinant les reducers du compteur et de la question via la fonction combineReducers, exportée par redux. Notez que le type des actions du compteur (initialement Action) a été renommé en CounterAction.

Grâce à combineReducers, dispatcher une action la propagera à tous les sous-reducers, pour reconstituer l'état global du store sous forme d'un objet.

type Action = CounterAction | QuestionAction;
 
const rootReducer = combineReducers({
  counter: counterReducer,
  question: questionReducer,
});
 
const store = createStore(rootReducer);
 
store.getState();
// {
//   counter: 0,
//   question: null,
// }

Les selectors

Voyons maintenant comment lire le store avec un peu plus de finesse que l'appel à store.getState(), qui renvoie l'intégralité de la state à un instant donné.

Il serait intéressant de pouvoir sélectionner une sous-partie de la state, par exemple juste notre question. Redux ne fournit aucune API pour faire ça, mais propose la notion de sélecteur : une fonction qui prend toute la state en paramètre, et en renvoie une sous-partie.

// we can infer the state's type
type State = ReturnType<typeof store.getState>;
 
const selectCounter = (state: State) => {
  return state.counter;
};
 
const selectQuestion = (state: State) => {
  return state.question;
};
 
const selectAnswers = (state: State) => {
  return selectQuestion(state)?.answers;
};

Notez qu'il est possible, et même souhaitable, de composer les sélecteurs.

Mais ne nous arrêtons pas en si bon chemin. En plus de pouvoir récupérer une sous-partie (une "slice") de la state, les sélecteurs permettent d'en calculer un état dérivé. Par exemple, on peut imaginer un sélecteur pour récupérer l'ensemble des réponses correctes.

const selectCorrectAnswers = (state: State) => {
  const answers = selectAnswers(state);
 
  return answers?.filter((answer) => {
    return answer.correct;
  });
};

Il est également possible de donner des paramètres aux sélecteurs, pour calculer des états dérivés selon des critères custom.

const selectAnswersMatching = (state: State, re: RegExp) => {
  const answers = selectAnswers(state);
 
  return answers?.filter((answer) => {
    return answer.text.match(re);
  });
};

Cette notion d'état dérivé est super importante, elle va avoir un impact profond sur la façon d'organiser, de structurer l'information contenue dans le store. Il faut chercher à représenter l'information sous forme brute, facile à manipuler, et en dériver un maximum de choses.

Par exemple, si on veut stocker une durée, on pourrait ajouter à notre store un objet contenant les clés hours, minutes et seconds. Mais plutôt que de stocker l'information sous cette forme, il serait préférable de ne ne stocker qu'un entier, le nombre de secondes, et d'utiliser des sélecteurs pour transformer cette valeur brute sous la forme d'un objet détaillant les heures, les minutes et les secondes.

Donc en somme, les sélecteurs et les reducers permettent d'abstraire la structure du store, les sélecteurs pour la lecture et les reducers pour l'écriture. Autrement dit, si la façon dont les données sont stockées doit évoluer, il n'y a qu'à ces deux endroits qu'il faudra modifier du code. On peut également faire le parallèle avec le design pattern repository, souvent utilisé pour abstraire la manière dont les données sont structurées dans une base de données.

Petite remarque par rapport aux applications frontend fortement liées à un applicatif backend, comme c'est le cas chez Gojob. Il peut être tentant de remplir le store directement avec la data retournée par les calls API, mais ce n'est pas une bonne approche. Il sera souvent préférable de passer par une étape de transformation, pour représenter l'information de manière différente pour mieux répondre aux besoins frontend.

Recap

Et voilà où on en est ! Dans une application réelle, le code serait bien sûr splitté sur différents fichiers.

import { combineReducers, createStore } from 'redux';
 
type Action<Type extends string, Payload> = { type: Type } & Payload;
 
function createAction<Type extends string, Payload>(type: Type, payload?: Payload): Action<Type, Payload> {
  return { type, ...payload } as Action<Type, Payload>;
}
 
type State = ReturnType<typeof store.getState>;
 
// counter
 
const increment = () => {
  return createAction('increment');
};
 
const incrementByAmount = (amount: number) => {
  return createAction('incrementByAmount', { amount });
};
 
type CounterAction = ReturnType<typeof increment | typeof incrementByAmount>;
 
const counterReducer = (state = 0, action: CounterAction): number => {
  if (action.type === 'increment') {
    return state + 1;
  }
 
  if (action.type === 'incrementByAmount') {
    return state + action.amount;
  }
 
  return state;
};
 
const selectCounter = (state: State) => {
  return state.counter;
};
 
// question
 
type Question = {
  text: string;
  answers: Answer[];
};
 
type Answer = {
  text: string;
  correct: boolean;
};
 
const setQuestion = (question: Question) => {
  return createAction('setQuestion', { question });
};
 
const setQuestionText = (text: string) => {
  return createAction('setQuestionText', { text });
};
 
const addAnswer = (answer: Answer) => {
  return createAction('addAnswer', { answer });
};
 
type QuestionAction = ReturnType<typeof setQuestion | typeof setQuestionText | typeof addAnswer>;
 
const questionReducer = (state: Question | null = null, action: Action): Question | null => {
  if (action.type === 'setQuestion') {
    return action.question;
  }
 
  if (!state) {
    return null;
  }
 
  if (action.type === 'setQuestionText') {
    return {
      ...state,
      text: action.text,
    };
  }
 
  if (action.type === 'addAnswer') {
    return {
      ...state,
      answers: [...state.answers, action.answer],
    };
  }
 
  return state;
};
 
const selectQuestion = (state: State) => {
  return state.question;
};
 
const selectAnswers = (state: State) => {
  return selectQuestion(state)?.answers;
};
 
const selectCorrectAnswers = (state: State) => {
  return selectAnswers(state)?.filter((answer) => answer.correct);
};
 
const selectAnswersMatching = (state: State, re: RegExp) => {
  return selectAnswers(state)?.filter((answer) => answer.text.match(re));
};
 
// store
 
type Action = CounterAction | QuestionAction;
 
const rootReducer = combineReducers({
  counter: counterReducer,
  question: questionReducer,
});
 
const store = createStore(rootReducer);

Et un exemple d'utilisation de notre système :

store.dispatch(
  setQuestion({
    text: 'What is the answer to life?',
    answers: [
      { text: 'There is no answer', correct: true },
      { text: '42', correct: false },
    ],
  }),
);
 
store.dispatch(addAnswer({ text: '51, obviously', correct: false }));
 
store.dispatch(incrementByAmount(123));
 
selectCounter(store.getState());
// 123
 
selectCorrectAnswers(store.getState());
// [
//   { text: 'There is no answer', correct: true }
// ]
 
selectAnswersMatching(store.getState(), /\d+/);
// [
//   { text: '42', correct: false },
//   { text: '51, obviously', correct: false },
// ]

Lien entre le store et React

Jusqu'à maintenant, on n'a fait que manipuler l'état abstrait de notre application via un store redux. Pour afficher et interagir avec cet état, il va falloir le lier à React afin que chaque mise à jour provoque un re-render des composants affectés par les valeurs ayant changé. Et ça, c'est le rôle de react-redux.

Il existe deux façons de connecter un composant React au store : avec le higher order component connect, et avec les hooks useSelector et useDispatch. Je vais laisser connect de côté, et me baser sur l'API des hooks, plus flexible et plus en phase avec la philosophie actuelle de React.

Petite précision en ce qui concerne les performances : si votre app manipule beaucoup de données via un store redux, et qu'une action vient modifier une toute petite partie de la state, alors uniquement les composants qui lisent cette partie vont être re-render.

Premièrement, tous les composants de notre app qui utilisent ces hooks doivent être enfants du provider redux, ce qui fait du store une grosse variable globale.

import { FunctionComponent } from 'react';
import { Provider as ReduxProvider } from 'react-redux';
 
const App: FunctionComponent = () => <ReduxProvider store={store}>{/* ... */}</ReduxProvider>;

Pour analyser le store redux d'une app web, installez le devtool sur firefox ou chrome

Pour lire l'état actuel du store (ou en récupérer un état dérivé), on va utiliser, sans surprise, le hook useSelector. Ce hook prend un sélecteur en paramètre, et l'appelle en faisant automatiquement le lien avec la state.

import { useSelector } from 'react-redux';
 
const counter = useSelector(selectCounter);
const correctAnswers = useSelector(selectCorrectAnswers);

Very good, mais comment appeler un sélecteur qui attend un paramètre en plus de la state ? Quelque chose dans ce style ?

const answersContainingANumber = useSelector((state: State) => {
  return selectAnswersMatching(state, /\d+/);
});

Hmm... Moi j'dis, on peut faire mieux. Que diriez-vous de wrapper useSelector, pour permettre de passer les params après le sélecteur ?

import { useSelector as useReduxSelector } from 'react-redux';
 
interface Selector<ReturnType, Parameters extends unknown[]> {
  (state: State, ...params: Parameters): ReturnType;
}
 
const useSelector = <ReturnType, Parameters extends unknown[]>(
  selector: Selector<ReturnType, Parameters>,
  ...params: Parameters
) => {
  return useReduxSelector((state: State) => selector(state, ...params));
};
 
// noice
const answersContainingANumber = useSelector(selectCorrectAnswers, /\d+/);

Une alternative serait de concevoir les selectors paramétrés comme des fonctions qui retournent des selectors sans paramètres (autres que la state). Cela nous permettrait de se passer du hook useSelector custom, mais ce qui n'est pas aligné avec la définition de type d'un selector selon reselect, une bibliothèque de référence pour créer des sélecteurs mémoisés.

Et voilà, on a terminé pour la lecture de la state. A chaque fois que la valeur retournée par un sélecteur va changer, un re-render sera automatiquement triggered pour mettre à jour les composants affectés par ce changement en conséquence. N'est-ce pas magnifique ?


Pour modifier l'état du store, nous l'avons vu, il faut dispatcher une action qui sera envoyée aux reducers, et produire le nouvel état de l'app. Pour cela, rien de plus simple, le hook useDispatch retourne directement la fonction dispatch du store, qu'on peut appeler où on veut.

const SomeComponent: FunctionComponent = () => {
  const dispatch = useDispatch();
 
  useEffect(() => {
    // set a question when the component is mounted
    store.dispatch(setQuestion(question));
  }, []);
 
  // render a button to increment the counter
  return <button onClick={() => dispatch(increment())}>Incrémenter le compteur</button>;
};

Bon à savoir : la fonction dispatch est stable (sa référence ne change pas) tant que le store donné au provider ne change pas non plus. Il n'est donc pas nécessaire de la passer en paramètre des tableaux de dépendances des hooks. Mais manque de pot, le plugin eslint vérifiant les dépendances des hooks ne peut pas être configuré pour accepter d'omettre dispatch. Il est donc préférable de le laisser quand même (et de toute façon, ça ne change rien).

Voilà le code final permettant de faire le lien entre le store et react.

import { FunctionComponent, useEffect } from 'react';
import { Provider as ReduxProvider, useDispatch, useSelector as useReduxSelector } from 'react-redux';
 
import question from './question.json';
 
interface Selector<ReturnType, Parameters extends unknown[]> {
  (state: State, ...params: Parameters): ReturnType;
}
 
const useSelector = <ReturnType, Parameters extends unknown[]>(
  selector: Selector<ReturnType, Parameters>,
  ...params: Parameters
) => {
  return useReduxSelector((state: State) => selector(state, ...params));
};
 
const Counter: FunctionComponent = () => {
  const dispatch = useDispatch();
  const value = useSelector(selectCounter);
 
  return (
    <div>
      <strong>Counter value: {value}</strong>
      <button onClick={() => dispatch(increment())}>increment</button>
      <button onClick={() => dispatch(incrementByAmount(5))}>increment by 5</button>
    </div>
  );
};
 
const Question: FunctionComponent = () => {
  const dispatch = useDispatch();
  const question = useSelector(selectQuestion);
 
  useEffect(() => {
    dispatch(setQuestion(question));
  }, [dispatch]);
 
  return (
    <div>
      <strong>Question: {question.text}</strong>
      <ul>
        {question.answers.map((answer, idx) => <li key={idx}>{answer.text}</li>)}
      </ul>
    </div>
  );
};
 
const store = /* ... */;
 
const App: FunctionComponent = () => (
  <ReduxProvider store={store}>
    <Counter>
    <Question />
  </ReduxProvider>
);

(désolé pour la coloration syntaxique, il faut qu'on fix le problème...)

Conclusion

Vous avez maintenant toutes les clés pour mettre en place la gestion de l'état d'une application web via redux ! Wouhou !

Mais redux tout seul n'est que la porte d'entrée donnant accès à tout un écosystème incroyablement riche autour de la gestion d'état. Une des briques essentielles pour aller plus loin est la gestion de l'asynchronicité, qui peut être mise en place de plusieurs manières, la plus simple étant certainement redux-thunk. Pour découvrir cette bibliothèque par l'exemple, je ne peux que recommander cet article, expliquant comment gérer l'état d'un aquarium connecté.

Un des acteurs majeurs de l'écosystème redux est bien sûr la bibliothèque redux toolkit, permettant de simplifier une grande partie des patterns expliqués dans cet article. Par contre, je trouve personnellement cette bibliothèque complexe, et je déconseille l'apprentissage simultané de redux et redux toolkit.

Enfin, le point central qu'apporte redux selon moi est d'être en mesure de découpler le code métier du reste. C'est-à-dire faire en sorte que le code métier, le code qui implémente le fonctionnement de l'app, ne soit pas couplé à react, fetch, window ou whatever else. C'est du pur TypeScript, qui ne connaît que redux. Et croyez-moi, c'est un vrai bonheur d'être capable d'apporter des modifications sur du code métier en étant pleinement confiant de n'avoir rien cassé.

Ce découplage entre domaine et infrastructure est un principe qui est au cœur de la clean architecture, et qu'il est possible d'implémenter de plusieurs manières, redux étant une approche que je trouve particulièrement intéressante. Tout ça sera expliqué dans un prochain article, où on implémentera les use cases pour répondre à la question contenue dans le store.

J'espère que cet article vous aura intéressé, si vous avez des questions / feedbacks, je suis toujours hyper partant pour discuter ! Cheers !