Redux sux

React et Redux sont pour beaucoup deux faces d'un tout immutables. Il est difficile de trouver une formation ou un projet où redux n'a pas été choisi comme outil de gestion d'état. Pourtant, force est de constater qu'il est la plupart du temps très mal utilisé. Nous allons d'abord voir pourquoi j'estime que son usage dépasse son objectif, et nous verrons quelles meilleures solutions existent.

The single source of truth

Redux est conçu pour gérer l'état global d'une application. Il est prévu pour permettre de partager des informations entre différentes parties de l'application mais il incite surtout à faire passer absolument toutes les informations d'une app par ce store. En théorie, les composants react ne contiennent absolument aucune logique métier de traitement des données et se consacrent strictement à l'interface pendant que redux met à disposition des actions qui sauront manipuler les modèles et les publier dans ce store.

Les limites à cette description idyllique arrivent quand on commence à vouloir découper son application en modules réutilisables. Redux fonctionne avec un seul et unique store. Pour permettre de greffer des reducers provenant de modules extérieurs, il faudra donc combiner le reducer supplémentaire au store de son application. Bref, en plus de devoir importer les composants du module, il faut aussi aller câbler les actions et les reducers de ce module. Pas très plug'n'play. Nous verrons dans la seconde partie comment cela pourrait être bien plus simple avec des techniques standards. Ma quête vers la modularité a été grandement motivée par le fait qu'il semble pour beaucoup obligatoire de faire passer par redux la moindre des données, même si une requête est faite par un seul et unique composant dans toute l'app et qu'elle n'a évidemment aucun besoin de partager son résultat avec le reste de l'app.

Un constat que j'ai pu faire aussi est que redux semble trop souvent utilisé comme outil pour faire passer des infos dans des composants sans les faire passer de parents à enfants. Tout simplement car Redux est bâti depuis toujours sur l'API context de react, qui était jusqu'au mois de Mars 2018, officiellement déconseillée d'usage par l'équipe de React. Un développeur cherchant une solution pour contextualiser des données et se tournant vers la doc de React context se voyait afficher un message anxiogène lui indiquant que cette API était vouée à disparaitre et qu'il ne fallait absolument pas l'utiliser. Redux était ainsi une solution plus sereine (alors que pourtant tout aussi dangereuse) pour arriver à ces fins.

Et c'est ainsi que redux est utilisé pour stocker tout plein de trucs dans un gros store global. Autant balancer des variables globales dans window, ça reviendrait à peu près au même.

Modularité

Je suis très souvent confronté à des architectures où plusieurs sites se partagent de nombreux modules d'accès aux données et une API identique. J'ai donc un regard un peu plus global que : "un projet" => "une app". C'est pourquoi j'ai toujours perçu redux comme un frein. J'avais besoin de contextualiser des données, mais dans des scope limités. Et surtout, j'avais envie que mes modules soient utilisables en ne faisant rien de plus qu'importer les composants de ce module. J'ai donc, dès Mars 2018, regardé du côté de React Context. Celui-ci permet de générer un Provider et un Consumer dont le but est justement de faire passer des données à travers un arbre de composants. J'ai donc par exemple un module Authentification publiant un composant AuthProvider et un hoc withAuth qui permettront à mes apps de gérer l'authentification de la même manière. Je peux de plus placer mon <AuthProvider> au niveau le plus cohérent avec mon app : soit à la racine si toute mon app doit changer de comportement selon l'auth, soit uniquement au niveau des routes privées.

L'autre aspect qui me plait beaucoup avec cette approche, c'est qu'on peut le plus simplement du monde gérer ses actions et les mises à jour de l'état du Provider avec async/await sans se taper un immonde reducer.

Je vous laisse jeter un œil à cet exemple qui montre un provider de compteur asynchrone qui est consommé par un composant qui affiche la valeur du compteur, et un autre composant qui appelle l'action d'incrémentation asynchrone.

Cet usage de Context et de async/await me permet de plus facilement découper mes fonctionnalités par modules et de garder côte à côte les composants et leurs stores. Ce qui rend évidemment bien plus simple la publication de chacun de ces modules dans des packages npm afin qu'ils soient réutilisés en toute simplicité dans d'autres applications.

Et ce qui me permet d'écrire cet article alors que mon expérience en react/redux est largement plus courte que la plupart de la communauté (j'ai commencé en décembre dernier), c'est que le futur du React va s'orienter officiellement vers ce genre de pratique grâce aux hooks useContext et useReducer qui permettront, en natif, sans lib supplémentaires, de gérer des stores modulaires tout en gardant la liberté de choisir le pattern de reducer ou de l'async/await.

Hadrien

Hi, I'm a french Javascript Lead Developer, Web Architect from Toulouse, France. I've worked for 12 years for many projects with YUI, AngularJS, Aurelia.io and now React and React native.

Toulouse, France https://hadrien.eu