Conception par tranches de fonctionnalités pour React : couches, règles d’importation et moment où s’arrêter
Découvrez comment les couches du Feature-Sliced Design, la règle d’import unidirectionnel, les API publiques simplifiées et les importations croisées @x permettent de dénouer les bases de code React, ainsi que le moment où il convient d’adopter uniquement une partie de cette approche.
Sources: Feature-Sliced Design in React: How to Structure a Scalable Codebase (and When It’s Overkill)
Presque tous les projets React qui survivent au-delà de leur deuxième année possèdent un dossier utils/ que personne ne veut ouvrir. On y trouve un formatteur de dates, un wrapper pour les tentatives répétées, une moitié de machine à états ainsi qu’un fichier helpers.ts qui importe des éléments provenant de trois dossiers fonctionnels, tandis que deux de ces dossiers l’importent à leur tour, ce qui empêche quiconque de déterminer ce dont dépend quoi. Le Design par tranches fonctionnelles (FSD) est une méthodologie conçue spécifiquement pour résoudre ce problème. Après avoir lu ceci, vous connaîtrez ses six couches, la règle d’import unique qui assure la majeure partie du fonctionnement, comment définir des limites entre les tranches pour permettre un refactoring sûr, et, ce qui est le plus important, comment décider si l’applicationnement complet du FSD en vaut la peine ou s’un sous-ensemble plus léger est un meilleur choix.
Pourquoi les dossiers basés sur les types entravent l’évolution
Le layout classique regroupe les fichiers en fonction de leur nature (composants, hooks, outils) plutôt que de leur utilisation. Cela convient bien pour une petite application, mais dès l’apparition de domaines métier réels, cela prend cette forme :
src/
components/ # 200 files, no domain grouping
hooks/ # useAuth next to useCartTotals
utils/ # the grab-bag
services/ # api calls that import from utils, which imports back
Dans une telle structure, deux problèmes surviennent. Premièrement, la question « À quel endroit ce fichier doit-il être placé ? » n’a pas de réponse claire, ce qui fait que tout ce qui est ambigu finit dans utils/. Deuxièmement, rien ne restreint la direction des dépendances, ce qui permet la formation de cycles. Imaginez que le service de commandes fasse appel à utils/user.ts, que ce dernier dépende de services/auth.ts, et que le service d’authentification importe à son tour le service de commandes. De tels cycles restent silencieux ; ils se manifestent généralement bien plus tard sous forme d’une valeur inattendue undefined lors du chargement initial d’un module, car un fichier du cycle a été évalué avant que celui dont il dépend ne soit prêt, et à ce moment-là le graphe est trop embrouillé pour être lu.
FSD résout ces deux problèmes en donnant à l’ensemble du projet une structure prédéfinie, plutôt que de se fier aux habitudes de chaque développeur. Au moment où ce texte est écrit, la version v2.1 est celle en vigueur ; consultez les documents officiels pour connaître d’éventuelles mises à jour ultérieures.
Les six couches et leur contenu
FSD organise le code en couches dans un ordre fixe, de haut en bas : App, Pages, Widgets, Features, Entities, Shared. Il existe également une ancienne couche Processes, mais elle est obsolète et peut être ignorée. Cet ordre n’a pas de caractère purement esthétique ; il définit qui peut dépendre de qui.
- App : routage, points d’entrée, styles globaux et fournisseurs.
- Pages : pages complètes, ou grandes sections de l’interface utilisateur liées à une route.
- Widgets : blocs d’interface utilisateur autonomes et complets, capables de gérer un cas d’utilisation entier par eux-mêmes.
Les quatre couches intermédiaires (Pages jusqu’à Entités) sont divisées en tranches, une par domaine métier, par exemple entities/user ou features/auth. Les catégories App et Shared ne possèdent pas de tranches. Chaque tranche est ensuite divisée en segments selon leur rôle technique : ui pour les composants, api pour les requêtes backend et les types de réponse, model pour les schémas et les bases de données, lib pour les outils utilisés uniquement au sein de cette tranche, et config.
La règle d’importation qui rend les cycles impossibles
La règle qui fonde tout le système est simple : un module ne peut importer que depuis des couches strictement inférieures à la sienne. Une page peut utiliser une fonctionnalité, mais une fonctionnalité ne peut jamais utiliser une page. De plus, les fragments de la même couche ne peuvent pas s’importer mutuellement du tout. À l’intérieur d’un même fragment, les fichiers peuvent se faire référence librement ; la restriction s’applique aux frontières entre fragments et couches.
Appliquons cette règle au cycle mentionné précédemment. Après la réorganisation en fragments, la logique de commande se trouve dans entities/order, la logique utilisateur dans entities/user, et le flux d’authentification dans features/auth. Les dépendances autorisées deviennent alors :
features/auth → can import entities, shared
entities/order → can import shared only
entities/user → can import shared only
Si entities/order tente d’importer depuis features/auth, il s’agit d’une violation structurelle de l’architecture et non d’une question de style. Un cycle ne peut pas se former, car chaque arête du graphe de dépendances pointe vers le bas. Cela modifie également la manière dont on analyse le code : au lieu de suivre les imports fichier par fichier, on pense en termes de six couches, un modèle que l’on peut réellement garder en mémoire.
Les APIs publiques maintiennent les détails internes des slices privés
Chaque slice expose une API publique, généralement un index.ts qui réexporte ce que les utilisateurs extérieurs peuvent utiliser. Les documents FSD sont clairs sur l’intention : ne divulguer que ce qui est nécessaire et conserver les détails d’implémentation cachés.
Une version très courante de ce fichier va à l’encontre de cet objectif :
// features/auth/index.ts (the anti-pattern)
export * from "./ui";
export * from "./model";
Les wildcards réexportent tout, y compris model/tokenStore.ts, qui devait rester privé. Tôt ou tard, une page importera directement le stockage de tokens, et à partir de là il deviendra impossible de le restructurer sans casser un utilisateur dont on ignorait l’existence. Les wildcards peuvent également rendre plus difficile le processus de suppression de code inutilisé par les outils de bundling, ce qui peut entraîner l’envoi de code inutilisé aux utilisateurs.
L’alternative consiste à énumérer explicitement chaque export :
// features/auth/index.ts (the boundary)
export { LoginForm } from "./ui/LoginForm";
export { useSession } from "./model/useSession";
// tokenStore stays internal. Nobody outside auth can touch it.
La slice dispose désormais de deux noms publics exactement. Tout le reste à l’intérieur peut être renommé, divisé ou réécrit sans affecter aucun appelant. Cette combinaison, un graphe de dépendances lisible associé à la liberté de modifier les éléments internes, constitue l’avantage principal promis par FSD.
La documentation FSD soulève une précaution importante : les fichiers index de type barrel peuvent entraîner des importations circulaires, provoquer des problèmes de compression dans shared/ui et shared/lib, ainsi que ralentir les outils de bundling dans les projets volumineux. La solution pratique consiste à conserver des API publiques explicites et minimalistes, et à faire particulièrement attention dans la section Shared, où il est préférable d’importer directement de nombreux modules petits et indépendants plutôt que de les faire passer par un seul fichier barrel de grande taille.
Importations croisées, découpage excessif et utilisation des outils de vérification
La notation @x pour les entités qui se référent mutuellement
Certaines entités dépendent légitimement les unes des autres ; une commande, par exemple, a besoin du type User. Comme les importations au même niveau sont interdites, la version 2.1 a standardisé la notation @x pour cette situation précise. Le slice user publie un point d’entrée dédié destiné au slice order, et ce dernier l’importe :
// entities/user/@x/order.ts
export type { User } from "../model/types";
// in entities/order/model/types.ts
import type { User } from "entities/user/@x/order";
Le nom du fichier rend cette relation explicite et facile à rechercher : entities/user/@x/order.ts correspond à la partie de user que order est autorisé à voir. La documentation considère cela comme une exception limitée. Limitez les importations croisées au strict nécessaire et n’utilisez @x qu’à la couche Entities. Si vous constatez en avoir besoin partout dans le code, c’est un signe que vos slices sont mal structurés.
Steiger indique à la fois les violations et le sur-engineering
Les règles ne sont utiles que si elles sont appliquées. Steiger, l’outil de vérification d’architecture pour FSD (ses règles sont fournies sous le nom @feature-sliced/steiger-plugin), fonctionne sans configuration et était encore en version bêta au moment de la rédaction. En plus de vérifier les règles d’importation, il empêche une sur-ingénierie : la règle insignificant-slice se déclenche lorsque une fonctionnalité ou une entité n’est utilisée que par une seule page, tandis que excessive-slicing indique lorsque le projet a été divisé en trop de petits éléments. Lorsque Steiger signale un élément insignifiant, suivez son conseil : vous avez créé un dossier alors qu’une simple fonction locale aurait suffi.
Lorsque FSD complet est un excès
Les noms des dossiers constituent la partie la moins importante de FSD. Copier les six couches dans une petite application crée son propre type de désordre : plus de fichiers que de logique, et chaque fonctionnalité dispersée dans quatre répertoires, ce qui transforme un simple changement en une tâche de recherche.
La spécification prend cela en compte, c’est pourquoi la version 2.1 recommande une approche « pages d’abord » pour l’adoption. Commencez uniquement avec Application, Pages, Widgets et Partagé, et envisagez de vous en tenir là. Pour de nombreuses applications, cela constitue déjà une structure complète et lisible.
Mouvez le code dans Features et Entities uniquement lorsqu’une raison concrète existe. Une heuristique utile consiste à attendre que la même logique soit nécessaire pour trois pages ou widgets au moins avant de lui attribuer un espace dédié. Jusqu’à ce moment-là, un flux de paiement entièrement situé dans pages/checkout est le choix approprié, et non une solution rapide. Une seule réutilisation peut être le fruit du hasard ; trois indiquent un véritable schéma, et c’est précisément pour représenter ces schémas réutilisables que la couche Features existe.
Si vous envisagez également FSD par rapport à des structures plus simples basées sur des dossiers de fonctionnalités ou des domaines, notre comparaison des schémas de structure de dossiers React présente ces alternatives côte à côte.
Une courte liste de vérification pour l’adoption
- Adoptez pleinement FSD lorsque l’équipe compte plus que quelques ingénieurs, que le nombre de domaines métier continue d’augmenter et que
utils/est déjà devenu un dépotoir. - Pour un produit petit avec un seul responsable clair, conservez la règle d’importation descendante et restreignez les API publiques, en évitant les couches supplémentaires.
- Rédigez chaque
index.tssous forme de liste explicite d’exports ; évitezexport *. - Réservez
@xaux références d’entité à entité et considérez son utilisation fréquente comme un problème de limite. - Exécutez un outil de linting comme Steiger afin que les règles, ainsi que les avertissements concernant les slices inutiles, soient appliqués automatiquement.
Conclusion
Les six dossiers n’étaient jamais l’objectif. Ce que FSD offre réellement, c’est un graphe de dépendances qui ne s’écoule qu’en une seule direction, ainsi que des composants dont les mécanismes internes sont cachés derrière de petits contrats, ce qui permet de maintenir une base de code compréhensible même dans un an. Vous pouvez obtenir la majeure partie de ces avantages uniquement grâce aux règles d’importation et aux API publiques, en n’ajoutant des couches supplémentaires que lorsque la réutilisation réelle l’exige.