Retour au blog

Créer un monorepo avec React

La structure des dossiers et la gestion des modules d'une application peuvent devenir très complexes à mesure que l'application se développe. Cette croissance peut rapidement devenir difficile à suivre. Un bon moyen de structurer l'application consiste à l'écrire en se basant sur les fonctionnalités, chaque fonctionnalité résidant à sa place.

Un modèle commun et simple consiste à diviser l'application dans différents packages, représentant chacun une fonctionnalité, un composant ou encore une API. Ces packages peuvent être partagés entre différentes applications.

Cette structure multi-packages est déjà utilisée par plusieurs grands noms de l'informatique et est connue sous le nom de monorepo. 

Les outils pour gérer le monorepo

Pour gérer ce genre d'architecture, différents outils existent :

  • Yarn (v1)
  • Lerna
  • Bazel (de Google)
  • Buck (de Facebook)

Yarn, avec son API Workspaces, ainsi que Lerna, sont très répandus. Ils permettent de gérer très facilement un projet de type monorepo.

Peut-être avez-vous déjà utilisé un outil qui possède lui-même ce type d’architecture. Babel et React en sont le parfait exemple : ils utilisent un référentiel unique qui facilite grandement la maintenance et la synchronisation des différents packages.

Les avantages du monorepo

  • L'approche d'un dépôt packages réduit la quantité de code répété qui doit être écrit et utilisé dans plusieurs packages.
  • Le partage et la réutilisation de code sont infiniment plus faciles : chaque package appartient au même référentiel et suit la même structure et le même processus de développement.
  • Le refactoring à grande échelle devient également plus simple. Un changement dans certaines API qui affecte plusieurs parties de la code-base peut être effectué en même temps.
  • Si le projet est développé avec une stack entièrement JavaScript, l'avantage est évidemment décuplé : partage de code entre le client web/natif et le serveur.

Les inconvénients du monorepo

Malgré les atouts incontestables de l'utilisation du monorepo, il existe quelques limites que nous souhaitions tout de même vous exposer avant d'entrer dans le vif du sujet.

  • L'intégration d'un nouveau développeur peut être plus difficile car il se retrouvera confronté à une énorme code-base.
  • Certaines limitations techniques du système de versionning peuvent survenir si le projet est conséquent (ex : la gestion de dizaines de milliers de fichiers). Par exemple, Facebook a forké Mercurial pour répondre à ce type de problématiques. Google a développé son propre système de contrôle de version distribué en interne.
  • La mise en place d'un CI/CD peut être un véritable défi sur ce type de projet et pourra prendre un peu de temps.

Exemple de création de monorepo avec React

Passons maintenant à notre exemple. React offre la possibilité de développer une application web et native grâce au concept interne des renderers. Un renderer est une logique de rendu du model React dans une target :

  • react-dom pour le web
  • react-native pour Android et iOS
  • react-native-windows pour Windows
  • react-360 pour la VR

Le partage de vues entre le web et le natif ne serait pas possible sans le travail de Nicolas Gallagher, développeur du module react-native-web. Celui-ci permet d'utiliser les API de react-native en web. Le module est activement maintenu et massivement utilisé. A titre d’exemple, la PWA de Twitter est intégralement développée avec ce module.

Voici un exemple d’architecture monorepo dans un projet avec une stack 100% JavaScript :

Détail des différents packages :

  • Admin : application web avec react-admin, connectée à l’API et fait office de back-office
  • API : API web en JavaScript (peu importe le langage)
  • Core: contient tout le core de notre application : constante, state, hook, etc.
  • Cypress : contient tous les tests E2E du projet dans un seul environnement
  • GraphQL: contient toutes les Query, Mutations, etc. de votre API. L’idée de faire un package isolé est qu’il pourra être utilisé sur chaque plateforme
  • Mobile: application native sur iOS et Android. Il est possible d’ajouter le support de Windows via react-native-windows ou de macOS via react-native-macos ou Catalyst. Le dossier pourrait être nommé native avec un support desktop.
  • Storybook: contient toutes les stories UI de chaque plateforme
  • UI : contient toute l’interface utilisateur de chaque plateforme. Il est possible de créer un fichier unique ou dédié à une plateforme : index.tsx, index.web.tsx, index.android.tsx, etc.
  • Web: application web rendue avec react-native-web

Il est possible d’aller encore plus loin, par exemple, en ajoutant un package nommé extension. Celui-ci serait une extension navigateur Chrome/Brave/Mozilla/Edge, ou encore un package CLI dédié au projet en passant par un package IoT !

Ce type d’architecture demande néanmoins un certain temps de configuration et d’adaptation : un seul dossier node_modules sera créé à la racine du projet. Cela donne ce type de configuration en utilisant l’API Workspaces de Yarn :

monorépo avec react

Comment ça fonctionne ?

Un fichier package.json doit être défini à la racine du projet. Celui-ci devra contenir la clé private à true ainsi que les différents workspaces du projet. Exemple en tenant compte de l’illustration ci-dessus :

{
  “name”: “my-app”,
  “private”: true,
  “workspaces”: [
    “packages/admin”,
    “packages/api”,
    …
  ]
}

Chaque package contient également son propre fichier package.json, qui doit obligatoirement contenir les clés name et version afin qu’il puisse être défini comme module du workspace. Exemple avec le core :

{
  “name”: “@my-app/core”,
  “version”: “0.0.1”
}

Avec cette architecture, l’installation des dépendances se fera dans un seul dossier node_modules à la racine du projet. Il est néanmoins possible que chaque package puisse avoir sa dépendance dans son propre dossier.

En savoir plus sur l’API Workspace de yarn

Conclusion sur le monorepo

Cette architecture ajoute une base solide dans un projet. Elle apporte un gain de productivité en :

  • permettant de modifier facilement et rapidement n’importe quelle partie de l’application
  • sachant exactement quelles parties ont été affectées

En dehors des packages, le tooling du projet est également centralisé : environnement de développement, dépendances, configuration des tests, les différents linter, CI/CD.

Ce modèle fait ses preuves : il permet le développement d’application par plusieurs équipes interdépendantes. Celles-ci peuvent travailler sur plusieurs packages, tout en gardant un scope dédié.