Retour d’expérience React Native : la gestion des données avec Redux
24 novembre 2021
Disclaimer : Cet article n’a pas vocation à blâmer, ni à déconseiller l’usage de Redux. Il s’agit simplement d’un retour d’expérience dont le but est de relever et pointer les inconvénients et/ou avantages rencontrés.
Contexte : une application mobile à livrer rapidement
Prenons d’abord le temps de contextualiser le projet sur lequel s’appuie ce retour d’expérience. Ce projet est une application mobile accompagnant des utilisateurs atteints de pathologies neurologiques. Elle regroupe diverses fonctionnalités, comme l’alimentation d’un journal de bord pour un suivi médicamenteux, une rubrique éditoriale sur des sujets en liens avec les maladies, des programmes de sophrologie ou encore physique. Il y a donc un certain nombre de données à traiter et à gérer, de différentes natures, et avec un réel besoin de profiter des services de l’application même lors de coupures réseaux.
Une mise en production en moins de quatre mois, une équipe généralement habituée à des projets orientés web, un certain nombre de choix ont été réalisés rapidement pour tenir les délais de livraison. Et avec ces choix, son lot de regrets, dont Redux fait partie aujourd’hui.
La mise en œuvre avec Redux
Il y a plusieurs façons de débuter des projets avec Redux. Et c’est bien là le problème. Redux se veut être une être solution “unopinionated”, autrement dit, “sans opinion” sur les pratiques et paternes que l’on peut adopter. Malgré cela, on retrouve quelques aides, qui peuvent s’avérer utiles sur la documentation de Redux.
Alors par où commencer ? Entre construire ses reducers manuellement, ou utiliser des librairies comme @reduxjs/toolkit ou encore reduxsauce, comprendre le fonctionnement des selectors et comment les optimiser correctement, en passant par la normalisation des données du store, sans oublier la puissance d’un Redux Saga pour gérer des side-effects tels que des requêtes API par exemple. Votre projet n’est pas encore commencé, qu’une migraine vous envahit soudainement. J’allais oublié, un sujet qui revient souvent sur la table, notamment lors de projet React, c’est la notion de l’immutabilité du store. Et nous n’avons pas encore parlé de la structure de notre store, car là aussi, il n’y a jamais de bonne façon de faire. Je suis sûr que vous l’entendez souvent, mais c’est important de le souligner “Tout dépend du besoin”. Avant d’aller voir d’autres alternatives (je vous vois fuir), laissez-moi vous compter l’histoire d’un projet où l’utilisation de Redux partait d’un bon sentiment.
Avec une approche naïve, le projet s’est construit sur un socle maison autour de Redux, sans librairies annexes. Jusque là, l’application n’avait que quelques mois, et le code source se portait plutôt bien. Et puis les évolutions sont arrivées, et les limites de Redux avec. Une structure fortement couplée (interdépendances des données), de la duplication dans tous les sens, une inconsistance de données (souvent incomplète) car pas toujours normalisées, mais surtout une croissance de données exponentielle sans moyen de nettoyer le store, avec bien entendu une sérialisation de moins en moins performante pour pouvoir persister les données et les conserver en mode hors-ligne. Sans oublier les erreurs sur la gestion de l’immutabilité, où quelques fois des données se retrouvaient mutables.
À ce moment précis, deux choix se présentent. Le premier est de mettre fin à cette souffrance et ce carnage, en remplaçant Redux par un autre outil. Le second, plus raisonné, serait une réorganisation et une normalisation de l’usage du store avec la mise en place de bonnes pratiques. Dans le cadre de ce retour d’expérience, c’est le second choix qui avait été tranché pour éviter une réécriture de plus de la moitié du projet. Cela ne veut pas dire que le premier choix n’est pas envisageable sur des gros projets. Encore une fois, tout dépend du besoin et de l’impact engendré par la solution retenue. Dans notre cas, la stabilisation de l’application était une priorité, et dans ce cadre là, un changement d’outil n’était pas envisageable pour plusieurs raisons, dont notamment l’expérience et la maîtrise des solutions alternatives, jugées non suffisantes pour justifier un tel tournant technologique. Dernier facteur également décisif, bloquer plusieurs jours ou même plusieurs semaines des développeurs sur une refonte du store est très coûteux, et in fine la valeur ajoutée au produit du point de vue de l’utilisateur final, n’est pas suffisante au regard des autres évolutions et améliorations prévues par la roadmap.
La réorganisation du store
Qui dit réagencement, dit avant tout, comprendre ce qui ne va pas et surtout où est-ce que ça ne va pas. Dans notre cas, le nombre et la complexité des sélecteurs ainsi que des reducers, ont été principalement ciblées. L’objectif premier de ce changement était avant tout d’éviter de reproduire une usine à gaz, pour permettre une meilleure maintenabilité et modularité des données. Le store a donc été découpé par domaine métier, de sorte à isoler les fonctionnalités et limiter au maximum le couplage de celles-ci. Je vous invite à lire l’article sur le refactor d’une architecture de fichiers pour aller plus loin sur ce point.
Vient alors la fameuse question de la responsabilité quant aux interactions avec les différents web services. Bien que plusieurs librairies proposent d’intégrer cette gestion à Redux, comme Thunk ou encore Saga, nous avons fait le choix de limiter Redux à sa fonction première, à savoir un simple conteneur d’état. En effet, étant déjà complexe dans sa prise en main et dans son usage, rajouter des responsabilités, à un outil qui n’est pas forcément adapté initialement, ne ferait qu’alourdir et complexifier la structure, déjà instable.
Limitée à sa fonction première, la normalisation et la structuration des données se posent alors. Pour y répondre, la question à se poser est simple : Comment mon application présente-t-elle les données ? Cette question permet de prédire facilement comment les données doivent-être structurées pour éviter des transformations, à la demande, qui sont souvent coûteuses en ressources. De manière générale, une normalisation standard se dégage souvent sur les projets, elle consiste en quatre points :
- Chaque type de données a sa propre « table » dans le store.
- Chaque « table de données » doit stocker individuellement les éléments dans un objet, avec les ID des éléments comme clés et les éléments eux-mêmes comme valeurs.
- Toute référence à des éléments individuels doit être effectuée en stockant l’ID de l’élément.
- Des tableaux d’ID doivent être utilisés pour indiquer l’ordre des références.
Pour en savoir plus sur cette approche de normalisation, Redux en parle de manière précise sur son site.
Bien que sans opinion, Redux met à disposition un guide des bonnes pratiques, et des techniques et outils à prendre en considération pour bien débuter un projet (https://redux.js.org/style-guide/style-guide). Très utile, et certainement incontournable, ce guide est peut être le signe d’un défaut majeur de la librairie, celui d’avoir recours à un certains nombre d’outils et pratiques annexes pour pouvoir être utilisé à bon escient.
Les Performances avec Redux
Nous voilà à l’un des sujets les plus tabous lorsqu’on parle de Redux : les performances. La documentation de la librairie le dit elle-même: “Redux may not be as efficient out of the box when compared to other libraries.”. C’est d’autant plus vrai lorsqu’on parle d’application mobile sur des téléphones Android entrée de gamme (soit un bon nombre d’utilisateurs encore aujourd’hui). Sur d’importantes quantités de données, comme c’est le cas ici, la réactivité du store et donc de l’application devient très vite impactée, pour la simple et bonne raison, que selon les périphériques, les ressources matérielles sont très limitées, notamment la RAM.
Problème adjacent, se servir de Redux comme solution de cache. Une leçon que nous retiendrons est que le store Redux se doit d’être éphémère. Deux explications à cela. La première est qu’il est difficile d’invalider une donnée, même si cette dernière est horodatée. À quel moment le faire et surtout comment le faire proprement sans impacter les performances de l’application ? Alors oui, une solution rapide serait de faire un reducer dont la responsabilité serait de supprimer des données, mais sur la base de quels critères ? Comment être sûr que la donnée sur le point d’être supprimée, n’est pas en cours d’utilisation ? Bloquer le chargement de l’application au lancement pour effectuer une tâche pareille, ce serait renoncer à une expérience utilisateur optimale.
La seconde explication concerne la persistance du store Redux. La sérialisation et la désérialisation, selon le volume de données, est une tâche gourmande en ressources, sans compter qu’il faut lire et écrire les données sur le périphérique pour un usage ultérieur. Parallèlement au premier point, lire et désérialiser un jeu de données volumineux prend du temps et donc alourdit le temps de chargement de l’application. Sachant par ailleurs que la majorité des données chargées ne sont pas forcément et immédiatement utiles dans l’expérience utilisateur. D’autant plus que cette technique ne permet pas une persistance différentielle de données, mais seulement complète, qui à pour conséquence d’être déclenchée à la moindre, même minime, modification de données dans le store Redux. Sans oublier les potentielles limites des supports de stockage, notamment avec Async Storage sous Android, où atteindre cette limite peut causer un certain nombre de problèmes à votre application. Depuis, cette limite peut être rehaussée facilement. Est-ce cependant une bonne idée d’augmenter la capacité ? Je vous laisse entre les mains de la documentation pour vous faire votre opinion : https://react-native-async-storage.github.io/async-storage/docs/advanced/db_size.
À ces explications, rajoutez l’accroissement des données, et vous obtiendrez une véritable bombe à retardement qui du jour au lendemain peut rendre votre application presque inutilisable, en tout cas difficilement exploitable.
Quelles alternatives existe-t-il ?
Alors que faire ? Redux peut être une bonne solution pour des applications web avec une durée de vie relativement courte, bien que difficile d’approche pour un premier projet. En revanche, pour une application mobile faite en React Native, il est préférable de se rapprocher de solutions mobiles adaptées et surtout éprouvées.
Nativement, les plateformes Android et iOS supportent très bien SQLite, qui présentent d’ailleurs plusieurs avantages :
- N’ajoute aucune surcharge au bundle de l’application.
- La première version date de Août 2000, donc SQLite a fait ses preuves.
- Bien maintenue avec des versions fréquentes.
- Open-source.
- Utilise le langage SQL, familier aux développeurs et administrateurs de bases de données.
- Cross-platform.
Les inconvénients de SQLite sont subjectifs et opiniâtres.
Si vous aimez l’aventure et les solutions tendances et modernes, il existe également un tas d’autres solutions qui ont fait leur preuves comme Realm, Firebase, ou encore Watermelon DB (basé sur SQLite) ainsi que Couchbase Lite.
Ces solutions ont plusieurs avantages, dont le premier, et non des moindres, c’est qu’elles sont prévu pour gérer des volumes de données tout en étant très performantes, ce sont des bases de données avec un support d’indexation, des modèles de données normalisés, qu’ils soient statiques ou bien dynamiques. Certaines, et même la majorité, proposent une synchronisation avec un système distant pour mettre à jour les données de manière transparente pour l’utilisateur (ajouts, modifications, suppressions).
Conclusion
En mobile, le système de gestion des données est un élément structurant à fort impact, qu’il soit technique, technologique, ou bien même lié à l’expérience utilisateur. Le choix est crucial, et certainement l’un des plus importants lorsqu’un projet mobile est lancé. Une fois en production, migrer un système de données n’est pas anodin, et nécessite très souvent la réécriture d’une bonne partie de l’application, pour ne pas dire une refonte de cette dernière. Préférez les solutions nativement embarquées par nos téléphones si cela est possible, comme SQLite, ou bien des solutions plus avant-gardistes pensées pour gérer une certaine volumétrie de données sur mobile.