Architecture de composants React : du chaos à la clarté
Atomic Design, composition, séparation des responsabilités... Comment structurer une codebase React qui scale.
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
useStatepour 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 :
- Context pour les données globales (user, theme)
- 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.
