#2 — L’application en accès libre
Certains de nos choix techniques méritent sans doute d’être partagés pour les prochaines expériences de start-up d’état ; le code source du pass est disponible sur le github de betagouv et nous avons développé quelques modules npm en accès libre concentrant des fonctionnalités utilisées au sein de nos deux applications web. Tout est sous une licence publique mozilla.
N.B. : Si vous n’avez pas déjà eu une présentation générale du projet, on vous suggère de commencer par la lecture de notre premier post.
Le pass Culture, quesaco
C’est en janvier 2018 qu’on a écrit les premières lignes de code du dépôt. Au bout de six mois, on avait une première version permettant d’engager la phase d’expérimentation. L’architecture est constituée essentiellement de 3 services :
- un serveur API avec sa base de données relationnelle, qui se charge d’enregistrer et redistribuer toutes les informations nécessaires à nos deux applications web quand il le faut.
- un site web backoffice Pro pour les professionnels, qui est à l’origine de la création manuelle de données dans le pass Culture. C’est à ce niveau qu’un acteur peut déclarer une offre culturelle (offer, dans la base de données) avec les descriptions nécessaires. Il y précise les stocks associés, ie le nombre de places disponibles, le prix et les dates de l’occasion quand il s’agit d’un évènement. Il lui est aussi fortement conseillé d’attacher une image, servant comme premier visuel de l’offre.
- une application web progressive Webapp destinée aux jeunes bénéficiaires du pass. C’est à ce niveau que l’application propose des recommandations (recommendations) d’offre à travers deux interfaces : un carrousel des recommandations les plus pertinentes selon le contexte, ainsi qu’une page de recherche, lui permettant de retrouver des offres particulières à sa demande.
Les applications web du pass Culture
Pour ne pas contraindre les jeunes de 18 ans à posséder obligatoirement un smartphone, l’application devait être en priorité une application web.
Plusieurs arguments nous ont conduits à développer le front en React. Il était important d’offrir dès le début une expérience utilisateur.ice séduisante pour les jeunes testeurs, habitués de tous les réseaux sociaux (snapchat, instagram, facebook…). Tout comme dans ces applications, nous voulions implémenter des transitions douces dans les navigations et faire que les interactions envoyant des requêtes vers le backend passent pour invisibles. Malgré la contrainte web, il est possible de simuler une telle expérience en codant le site comme une application progressive avec un format de single page application.
React n’est bien sûr pas le seul outil permettant ce cadre de travail, mais sa popularité actuelle et celle de ses librairies satellites (react-router, react-redux, …) a joué en sa faveur. On aimait bien aussi l’idée de pouvoir basculer facilement vers react-native si jamais il était nécessaire un jour de coder des animations plus gourmandes en ressource. Il fallait aussi coder en parallèle le site backoffice pour les professionnels, qui rassemble un grand nombre de pages de formulaires à champs complexes. Les Components React sont dans ce cadre très pratiques pour écrire des bouts de code réutilisables entre les pages. (note pour la suite de l’article : nous utiliserons le mot “vue” pour désigner toute fonction ou Component React permettant de rendre le html dans l’application).
react-redux
À petite équipe, mieux vaut réduire les surfaces d’api à retenir et à faire connaître aux nouveaux arrivants. Le backoffice et l’application jeune sont deux services web distincts mais il était important que leurs librairies externes utilisées, structures et règles de syntaxe ne divergent pas trop.
Dans cette logique, il nous a été utile d’organiser de la même façon la gestion de leurs flux de données grâce à un environnement react-redux et redux-saga. Chaque application possède un cache de données (un state) créé dynamiquement au montage du service, évoluant en fonction des interactions sur le site. La magie de react-redux nous dispense de coder le fait d’avertir les différentes vues, quand le cache change au cours du temps : on écrit comment les composants react viennent sélectionner dans le state les informations spécifiques pour remplir leur fonction d’affichage, et cela suffit.
Dans la majorité des pratiques, chaque application code ses fonctions reducer. Ce sont ces fonctions qui vont décider comment faire évoluer le state à partir de leur observation sur les actions émises par les vues. Prenons un exemple dans le backoffice :
Le bouton “Gérer les dates et les stocks” émet lorsqu’on clique dessus une action SHOW_MODAL informant un reducer modal de passer la propriété state.modal.isActive de false à true. Le changement de cette variable avertit automatiquement le Component Modal d’apparaître sur l’écran, ainsi que les éléments derrière la fenêtre de ne plus être manipulables par l’utilisateur.ice, tant que la fenêtre est visible.
Votre mission, si toutefois vous l’acceptez…
L’histoire se complexifie quand il s’agit d’interactions chargées de modifier de manière persistante la donnée côté base de données backend. Rien ne vaut un exemple, prenons le cas où un acteur culturel ajoute dans le backoffice un stock supplémentaire pour une de ses offres :
Comprenons bien le problème ici : le clic sur le bouton “Valider” entraîne une requête à l’api pour créer une nouvelle date. Si rien de spécial n’est codé côté frontend, nous tombons dans une situation où l’écran affiche toujours deux dates, alors qu’il y en a bien trois désormais dans la base. Tant que l’utilisateur.ice ne rafraîchit pas de lui-même la page, le backoffice affiche un état désynchronisé de la donnée.
Pour éviter cela, nous avons écrit un reducer data chargé d’agréger toutes les informations liées aux entités de la base de données. Celui-ci doit s’occuper de garder une empreinte toujours à jour des offers, stocks, recommendations manipulés pendant la session. C’est sur ce dernier point que nous avons trouvé des règles de gestion intéressantes à partager. Décomposons les étapes essentielles du code permettant cet ajout de date. Tout d’abord, au montage de la page :
- Nous sommes sur l’url /offres/F5UQ, F5UQ étant l’id d’une offre particulière. La vue principale appelle une action redux REQUEST_DATA_GET_/OFFERS/F5UQ demandant au backend les informations spécifiques liées à cette offre. Pour rappel, le code du pass Culture est ouvert : si vous regardez de plus près, vous verrez qu’on utilise un créateur d’action, requestData, auquel on passe les configurations nécessaires pour la requête : notamment l’apiPath (ie la route de l’api) et la méthode du protocole (DELETE, GET, PATCH, POST).
- redux-saga, à l’écoute de cette action, exécute de manière asynchrone un fetch GET /offers/F5UQ.
Cette requête retourne un paquet de données dans lequel figure au premier niveau les détails de l’offre (nom, durée de l’évènement, lieu…) et, à des niveaux encapsulés, des précisions sur les entités qui sont liées à l’offre, comme les stocks.
- redux-saga finit son travail en appelant une nouvelle action signant le succès de la requête : SUCCESS_DATA_GET_/OFFERS/F5UQ, qui est chargée d’enrichir le state.data avec ces nouvelles données.
- Les entités enregistrées dans le state data sont normalisées (ie que les stocks encapsulés dans le payload sont mis à plat dans le cache), et ceci dans un intérêt expliqué par la suite.
- Le Component StocksManager est en mesure désormais d’aller chercher ces données pour afficher deux stocks dans la fenêtre, via des sélecteurs.
Il vient ensuite les étapes caractérisant l’ajout d’un nouveau stock:
- Au moment où la personne a fini de remplir le formulaire d’ajout et qu’il clique sur Valider, le code déclenche une action, similaire à la première introduite REQUEST_DATA_GET_/OFFERS/F5UQ. L’action s’appelle REQUEST_DATA_POST_/STOCKS, car dans ce cas on spécifie un apiPath /stocks avec une méthode POST. En outre, le corps de la requête possède les informations du formulaires.
Le flux de données est entrepris de la même façon: le retour de la requête POST /stocks déclenche une action SUCCESS_DATA_POST_/STOCKS qui rafraîchit l’état global de l’application en lui ajoutant un objet stock dans son tableau state.data.stocks.
- le StocksManager et le compteur de stocks qui sont à l’écoute du state.data.stocks se mettent à jour grâce au flot de react-redux.
Une seule solution, généralisons
En résumé, nous nous sommes rendus compte que nous étions souvent confrontés aux mêmes besoins quand il s’agissait de synchroniser l’état de chaque webapp avec celui de l’API. Nous en sommes venus à écrire des actions stéréotypées déclenchées aussi bien au début qu’à la fin de chaque transaction avec le backend. Une logique redux-saga assure le crochet asynchrone entre les actions REQUEST_DATA_(GET|PATCH|POST|DELETE)_<COLLECTION NAME> et celles SUCCESS_DATA_(GET|PATCH|POST|DELETE)_<COLLECTION NAME> (nous avons aussi des actions FAIL du même genre pour prendre en charge les requêtes retournant des status 400 ou 500).
De cette manière le state.data devient un objet reflétant un état toujours synchronisé de la donnée backend. Il ne reste pour les développeurs.ses que le travail métier d’appeler aux bons endroits les créateurs d’action requestData(en écrivant notamment cette logique dans les mapDispatchToProps), ainsi que l’accès spécifique à ces données via l’écriture de sélecteurs dans les mapStateToProps.
La librairie redux-saga-data contient les trois objets nécessaires pour faire marcher n’importe quel projet avec ce cadre: createDataReducer et watchDataActions à utiliser respectivement au moment du montage de redux et saga dans l’application, ainsi que le déjà vu requestData. Une codesandbox est accessible ici pour en montrer l’usage. C’est une preuve de concept rejouant de manière plus simple l’exemple d’ajout de stock montré plus haut.
On comprendra l’intérêt d’avoir normalisé les données stocks dans ce cas. Sans cela, les stocks de l’offre seraient toujours contenus dans l’objet state.data.offers égale à [{ id: “F5UQ”, stocks: […] }], et il nous aurait fallu écrire dans le fichier mapDispatchToProps.js une méthode de résolution permettant de mettre à jour cette sous-liste en lui ajoutant un nouveau stock. Une manière de le faire est montrée ci-dessous l.43–45:
Au lieu d’écrire un resolver, on préfère donc dans ce genre de cas spécifier l’option normalizer, permettant de mettre à plat les entités stocks dans state.data. Ci-dessus, dans le code commenté, la clé “stocks” dans le normalizer signifie que le tableau d’objets du payload récupérés à cet endroit sera placé dans state.data à la clé précisée par stateKey (égale à “stocks”). Dans ce cas, précisément, c’est comme cela que action.payload.datum.stocks est fusionné dans state.data.stocks.
Généralisons encore plus
Mettre à jour un état normalisé des données backend est un besoin récurrent dans le monde des applications single page. On aimerait se dire que peu importe la librairie de gestion d’actions que l’on choisit finalement, il serait idéal d’avoir le code de ce concept dans une librairie coeur. C’est ce que nous avons fait avec fetch-normalize-data. redux-saga-data n’est finalement qu’un wrapper de cette librairie pour le faire marcher avec redux-saga.
En outre, on a choisi d’utiliser redux-saga pour cette gestion asynchrone des actions. C’est en vérité un choix toujours discutable, au regard d’autres librairies qui seraient plus légères pour faire cela, comme redux-thunk. Du coup, pour ceux et celles qui préfèrent, une librairie redux-thunk-data existe aussi (voir la codesandbox)! Enfin, pour les personnes qui sont vraiment “reacto centrés” (et qui veulent au maximum alléger leur bundle), on a fait une librairie react-hook-data faisant la même chose et qui utilise les nouvelles api de React comme useEffect et useState ainsi que Context (voir sa codesandbox).
Conclusion
Le pass Culture est un projet technique jouant avec les derniers outils “à la mode” (webpack, React, redux, sass…) pour le front. Certains jours de développement se vivent comme dans un laboratoire de recherche pour JavaScript, qui s’inscrit dans la démarche de la communauté betagouv et plus généralement de celle du code en accès libre. Nous aimerions que notre travail contribue à l’idée que l’on peut construire des outils partagés facilitant le développement de communs logiciels.
On espère que cet article éveillera l’attention de celles et ceux curieux des enjeux techniques rencontrés dans le projet. Il existe encore d’autres librairies npm en accès libre sur betagouv que nous avons développées, mais cela sera sans doute l’affaire d’autres posts.
“Et pourquoi pas avoir choisi tout bonnement du graphql et react-appollo ? Ca marcherait pas avec du realm, pour faire un peu plus de privacy by design ? Vue.js c’est bien aussi !” Tout reste ouvert. Le pass Culture est encore à l’état de produit minimal, et nous avons montré ici juste quelques outils qui nous ont permis de passer le premier cap de la consolidation technique. Mais c’est un projet de grande envergure — il existe aussi des enjeux python avec l’algorithme de recommandation en cours de construction — une chose seule est sûre : on recrute ! Si vous voulez contribuer à la discussion, voire participer à l’évolution technique du pass, envoyez nous un mail (support [at] passculture.app)