Yasin Avci

Yasin Avci

Développeur Full-Stack

Paris, France

Me contacter
Retour aux articles
ReactArchitectureFrontend

Architecture de composants React : du chaos à la clarté

Atomic Design, composition, séparation des responsabilités... Comment structurer une codebase React qui scale.

25 avril 20245 min de lecture

Une application React peut rapidement devenir un enchevêtrement de composants. Après des dizaines de projets, voici les principes qui maintiennent le code organisé.

Le problème des composants fourre-tout

On commence tous pareil : un composant UserProfile.tsx de 50 lignes. Six mois plus tard, il en fait 500, gère l'affichage, les appels API, la validation, les modales...

// ❌ Le composant qui fait tout
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [isEditing, setIsEditing] = useState(false);
  const [formData, setFormData] = useState({});
  const [errors, setErrors] = useState({});
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => { /* fetch user */ }, []);

  const handleSubmit = async () => { /* validation + API */ };
  const handleDelete = async () => { /* confirmation + API */ };

  // 200 lignes de JSX avec des conditions partout
}

Principe 1 : Séparer présentation et logique

Composants de présentation (UI)

Ne font que de l'affichage. Reçoivent des props, retournent du JSX.

// components/ui/UserCard.tsx
interface UserCardProps {
  name: string;
  email: string;
  avatarUrl: string;
  onEdit: () => void;
}

function UserCard({ name, email, avatarUrl, onEdit }: UserCardProps) {
  return (
    <div className="rounded-lg border p-4">
      <img src={avatarUrl} alt={name} className="h-16 w-16 rounded-full" />
      <h3 className="text-lg font-semibold">{name}</h3>
      <p className="text-slate-500">{email}</p>
      <button onClick={onEdit}>Modifier</button>
    </div>
  );
}

Caractéristiques :

  • Pas de useState pour la logique métier
  • Pas d'appels API
  • Facilement testables et réutilisables

Composants conteneurs (Features)

Gèrent la logique : état, effets, appels API.

// features/user/UserProfileContainer.tsx
function UserProfileContainer({ userId }: { userId: string }) {
  const { user, isLoading } = useUser(userId);
  const { mutate: updateUser } = useUpdateUser();
  const [isEditing, setIsEditing] = useState(false);

  if (isLoading) return <UserCardSkeleton />;
  if (!user) return <UserNotFound />;

  return (
    <>
      <UserCard
        name={user.name}
        email={user.email}
        avatarUrl={user.avatarUrl}
        onEdit={() => setIsEditing(true)}
      />
      {isEditing && (
        <UserEditModal
          user={user}
          onSave={updateUser}
          onClose={() => setIsEditing(false)}
        />
      )}
    </>
  );
}

Principe 2 : La composition plutôt que l'héritage

Le pattern Compound Components

Pour les composants complexes avec plusieurs parties liées :

// Utilisation
<Card>
  <Card.Header>
    <Card.Title>Titre</Card.Title>
    <Card.Description>Description</Card.Description>
  </Card.Header>
  <Card.Content>
    Contenu principal
  </Card.Content>
  <Card.Footer>
    <Button>Action</Button>
  </Card.Footer>
</Card>
// Implémentation
const Card = ({ children, className }) => (
  <div className={cn("rounded-lg border", className)}>{children}</div>
);

Card.Header = ({ children }) => (
  <div className="border-b p-4">{children}</div>
);

Card.Title = ({ children }) => (
  <h3 className="text-lg font-semibold">{children}</h3>
);

// etc.

Avantages :

  • Flexibilité maximale
  • API intuitive
  • Chaque partie est simple

Le pattern Render Props / Children as Function

Quand le parent doit contrôler le rendu :

<DataTable
  data={users}
  columns={columns}
  renderRow={(user, index) => (
    <TableRow key={user.id} highlight={index % 2 === 0}>
      <TableCell>{user.name}</TableCell>
      <TableCell>{user.email}</TableCell>
    </TableRow>
  )}
/>

Principe 3 : Structure de dossiers scalable

Par fonctionnalité, pas par type

// ❌ Par type : difficile à naviguer
components/
  buttons/
  cards/
  modals/
  forms/
hooks/
  useUser.ts
  useProducts.ts

// ✅ Par fonctionnalité : cohérent
features/
  auth/
    components/
      LoginForm.tsx
      SignupForm.tsx
    hooks/
      useAuth.ts
    api/
      auth.ts
  products/
    components/
      ProductCard.tsx
      ProductList.tsx
    hooks/
      useProducts.ts

Les composants partagés à part

src/
  components/
    ui/           # Composants génériques (Button, Card, Modal)
    layout/       # Header, Footer, Sidebar
  features/       # Logique métier par domaine
  lib/            # Utilitaires, API clients
  hooks/          # Hooks partagés entre features

Principe 4 : Des props explicites

Éviter le prop drilling excessif

// ❌ Props qui traversent 5 niveaux
<App user={user}>
  <Layout user={user}>
    <Sidebar user={user}>
      <UserMenu user={user} />
    </Sidebar>
  </Layout>
</App>

Solutions :

  1. Context pour les données globales (user, theme)
  2. Composition pour passer les composants directement
// ✅ Composition : le parent contrôle
<Layout
  sidebar={<Sidebar><UserMenu user={user} /></Sidebar>}
>
  <MainContent />
</Layout>

Typer les props rigoureusement

// ❌ Trop permissif
interface ButtonProps {
  variant?: string;
  size?: string;
}

// ✅ Types stricts
interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
}

Exemple concret : refactoring d'une page

Avant (500 lignes)

function ProductPage({ productId }) {
  // 15 useState
  // 5 useEffect
  // 10 handlers
  // 300 lignes de JSX avec des ternaires imbriqués
}

Après (structure claire)

// pages/products/[id].tsx - 30 lignes
function ProductPage({ productId }) {
  return (
    <ProductProvider productId={productId}>
      <ProductLayout>
        <ProductHeader />
        <ProductGallery />
        <ProductInfo />
        <ProductActions />
        <ProductReviews />
      </ProductLayout>
    </ProductProvider>
  );
}

// features/products/context/ProductProvider.tsx
// Gère tout l'état et les API calls

// features/products/components/ProductInfo.tsx
// Affichage pur, consomme le context

Les signaux d'alerte

Il est temps de refactorer quand :

  • Un composant dépasse 200 lignes
  • Vous copiez-collez du code entre composants
  • Les props traversent plus de 2 niveaux
  • Vous avez du mal à trouver où modifier quelque chose
  • Les tests sont impossibles à écrire

Conclusion

Une bonne architecture de composants n'est pas une question d'outils ou de patterns à la mode. C'est une discipline : séparer les responsabilités, nommer clairement, organiser logiquement.

Commencez simple. Refactorez quand la douleur apparaît. Ne sur-architecturez pas un prototype.