Retour au cours

frontend / react

Architecture d'une grosse application React

Leçon 211 exercice

Explication

Ce que vous allez apprendre

  • Comparer une organisation par type de fichier et une organisation par feature
  • Créer un barrel export qui définit l'API publique d'une feature
  • Isoler la couche réseau derrière des fonctions dédiées, indépendantes du client HTTP utilisé
  • Séparer un composant container (logique) d'un composant présentation (affichage pur)
  • Construire un design system interne pour éviter la dérive visuelle entre équipes

Dans quel contexte ?

Après six mois de développement, le dossier src/components/ de l'application compte plus de 200 fichiers plats, tous mélangés : composants du panier, de l'authentification, du tableau de bord. Un nouveau développeur met une demi-journée à comprendre où ajouter un champ dans le formulaire d'inscription, car la logique associée est éclatée entre trois dossiers différents (components/, hooks/, utils/). Réorganiser par feature (features/panier/, features/authentification/) regroupe tout ce qui concerne une même fonctionnalité au même endroit.

Étape 1 : les hooks ne suffisent pas à grande échelle

Maîtriser JSX, les hooks et le state management ne suffit pas à garantir qu'une application reste facile à faire évoluer une fois qu'elle atteint plusieurs centaines de composants.

Étape 2 : ce qui se dégrade sans organisation

Sans organisation réfléchie, un projet finit par devenir un enchevêtrement où modifier une fonctionnalité sans en casser une autre demande de plus en plus d'efforts.

Étape 3 : une organisation naïve, par type de fichier

Une organisation naïve regroupe les fichiers par TYPE technique : tous les composants ensemble, tous les hooks ensemble. Cela oblige à naviguer entre de nombreux dossiers éloignés.

Étape 4 : l'alternative, organiser par feature

L'alternative recommandée à grande échelle organise le code par FEATURE : tout ce qui concerne le panier vit au même endroit, ce qui garde la logique liée physiquement proche.

Étape 5 : le rôle d'un barrel export

Un "barrel export" définit précisément ce qu'une fonctionnalité expose vers l'extérieur, en gardant tout le reste comme un détail d'implémentation interne.

Bonne pratique

Importe toujours depuis le barrel export d'une feature (import { usePanier } from "@/features/panier"), jamais directement depuis un fichier interne (@/features/panier/hooks/usePanier). Ça garde la liberté de restructurer l'intérieur d'une feature sans casser le reste de l'application.

OrganisationAvantage principalLimite
Par type de fichier (components/, hooks/)Simple à comprendre au démarrageLogique liée éclatée dans plusieurs dossiers
Par feature (features/panier/)Logique liée regroupée, scalableDemande une discipline d'équipe (barrel exports)

Étape 6 : isoler la couche réseau

De la même façon, isoler la couche réseau derrière des fonctions dédiées limite l'impact d'un futur changement d'outil HTTP à un seul fichier.

Un dernier principe, séparer logique et présentation

Distinguer les composants "container" des composants "présentation" facilite grandement les tests : un composant de présentation pur se teste isolément.

Et ensuite ?

Une fois l'architecture posée, la prochaine étape traite d'un sujet de robustesse : empêcher qu'une erreur ne fasse planter toute l'application, avec les Error Boundaries.

Commandes & code

Architecture d'une grosse application React

Organiser des centaines de composants sans que le projet devienne ingérable.

bash
# Organisation par FEATURE plutôt que par TYPE de fichier -- scalable au-delà de quelques écrans
src/
  features/
    panier/
      components/
        ResumePanier.jsx
        LignePanier.jsx
      hooks/
        usePanier.js
      api/
        panierApi.js
      panierSlice.js
      index.js          # point d'entrée public de la feature (barrel export)
    authentification/
      components/
      hooks/
        useAuth.js
      api/
        authApi.js
  composants-partages/    # UI générique, sans logique métier : Bouton, Modale, Input
    Bouton.jsx
    Modale.jsx
  lib/                    # utilitaires purs, sans dépendance React
    formatage.js
    validation.js
  routes/
jsx
// Barrel export : contrôle explicite de ce qu'une feature expose vers l'extérieur
// features/panier/index.js
export { ResumePanier } from "./components/ResumePanier";
export { usePanier } from "./hooks/usePanier";
// tout le reste (panierSlice, panierApi...) reste un détail d'implémentation INTERNE à la feature

// Ailleurs dans l'app : import uniquement via le point d'entrée public
import { ResumePanier, usePanier } from "@/features/panier";
// jamais : import { panierSlice } from "@/features/panier/panierSlice" -- casse l'encapsulation
jsx
// Couche API isolée : les composants ne connaissent jamais fetch/axios directement
// features/panier/api/panierApi.js
export async function recupererPanier() {
    const res = await fetch("/api/panier");
    if (!res.ok) throw new Error("Impossible de charger le panier");
    return res.json();
}

export async function ajouterArticle(article) {
    const res = await fetch("/api/panier/articles", {
        method: "POST",
        body: JSON.stringify(article),
    });
    return res.json();
}

// features/panier/hooks/usePanier.js
export function usePanier() {
    return useQuery({ queryKey: ["panier"], queryFn: recupererPanier });
}
// changer de client HTTP (fetch -> axios) ne touche QU'un seul fichier
jsx
// Composants "container" (logique) vs "présentation" (affichage pur) -- séparation utile à grande échelle
function ResumePanierContainer() {
    const { data: panier, isLoading } = usePanier();
    if (isLoading) return <Squelette />;
    return <ResumePanierUI articles={panier.articles} total={panier.total} />;
}

// composant purement présentationnel : testable isolément, réutilisable, sans dépendance réseau
function ResumePanierUI({ articles, total }) {
    return (
        <div>
            {articles.map((a) => <LignePanier key={a.id} article={a} />)}
            <p>Total : {total}€</p>
        </div>
    );
}
jsx
// Design system interne : centraliser les primitives UI évite la dérive visuelle
// composants-partages/Bouton.jsx
function Bouton({ variante = "primaire", taille = "md", ...props }) {
    return (
        <button
            className={`bouton bouton--${variante} bouton--${taille}`}
            {...props}
        />
    );
}
// toute l'app utilise CE bouton -- un changement de style se propage partout,
// et aucune équipe ne réinvente son propre bouton légèrement différent

Résumé

  • Organiser par feature (et non par type de fichier) garde la logique liée physiquement proche dans le repo.
  • Un barrel export (index.js) par feature impose une frontière claire entre API publique et détails internes.
  • Isoler la couche réseau derrière des fonctions dédiées limite l'impact d'un changement de client HTTP.
  • Séparer composants "container" (logique) et "présentation" (UI pure) facilite les tests et la réutilisation.

Exercices pratiques

1 disponible
1

Mission : refermer une brèche dans le barrel export du panier

Objectif : Identifier et corriger un import qui contourne le barrel export d'une feature, puis restructurer un composant mêlant logique et présentation.

Contexte

Une revue de code découvre, dans src/features/authentification/components/ProfilMenu.jsx, la ligne import { panierSlice } from "@/features/panier/panierSlice"; — un import direct qui traverse la frontière entre deux features en ignorant complètement le barrel export features/panier/index.js. Une autre revue relève que ResumePanier.jsx mélange dans un seul composant l'appel réseau via usePanier() et tout l'affichage détaillé du panier, ce qui rend le rendu impossible à tester sans mocker le réseau.

Résoudre l’exercice →