Le développement orienté composants renverse l’approche descendante traditionnelle de la construction d’interfaces. Au lieu de construire des pages puis d’en extraire les parties réutilisables, vous partez de composants individuels que vous assemblez en vues.
Le bénéfice ne se limite pas à la réutilisation. Lorsqu’un composant est construit et éprouvé de manière autonome, ses cas limites apparaissent tôt, son API doit être explicite, et la page qui finit par l’utiliser relève surtout de l’assemblage. Le prix à payer est un peu plus de réflexion en amont sur les frontières, et cet article cherche à le réduire.
La hiérarchie des composants
Organisez vos composants selon une hiérarchie claire : atomes (boutons, champs, libellés), molécules (barres de recherche, groupes de formulaire), organismes (navigation, sections hero), templates (mises en page) et pages (instances spécifiques). Cette méthodologie d’atomic design, popularisée par Brad Frost, fournit un vocabulaire commun pour parler d’architecture d’interface.
Traitez cette hiérarchie comme un vocabulaire, pas comme une arborescence de dossiers à respecter. Les équipes s’enlisent souvent à débattre pour savoir si une carte est une molécule ou un organisme. Pour le code, un découpage plus utile est le suivant :
- Primitives, qui ne savent rien de votre domaine métier :
Button,Input,Badge,Dialog. - Compositions, qui agencent des primitives en un motif reconnaissable :
SearchField,StatCard,EmptyState. - Fonctionnalités, qui relient des compositions à de vraies données et à un vrai comportement :
InvoiceTable,CheckoutSummary.
La règle qui garde l’ensemble sain : les dépendances ne pointent que vers le bas. Une primitive n’importe jamais une fonctionnalité, et une composition ne récupère jamais de données.
Développer en isolation
Construisez et testez vos composants en dehors du contexte d’une page complète. Des outils comme Storybook, ou l’API Container d’Astro pour rendre un composant seul, vous permettent d’afficher vos composants avec différentes props, différents états et différentes variations de données. Vous détectez ainsi les cas limites tôt et obtenez des composants plus robustes.
L’isolation vous oblige à répondre à des questions qu’une page complète vous laisse ignorer. À quoi ressemble le tableau avec zéro ligne ? Avec une seule ligne dont le nom fait 80 caractères ? Pendant le chargement ? Après l’échec de la requête ? Notez chacun de ces cas comme un état nommé et affichez-le :
export const states = {
loading: { status: 'loading' },
empty: { status: 'ready', rows: [] },
error: { status: 'error', message: 'Could not load invoices.' },
long: { status: 'ready', rows: [longNameRow, ...sampleRows] },
};
Le développement isolé rend aussi les tests de régression visuelle praticables. Prenez des captures de chaque état de vos composants et détectez automatiquement les changements visuels involontaires d’une version à l’autre. Gardez des captures ciblées, un état de composant par image, pour qu’un diff en échec désigne directement la cause plutôt qu’une page entière.
Props et flux de données
Concevez des API de composants claires, avec des props bien définies. Utilisez des interfaces TypeScript pour documenter les entrées attendues et fournissez des valeurs par défaut sensées. Pour la plupart des composants, privilégiez le passage explicite de props plutôt que le contexte ou l’état global : le flux de données reste traçable et les composants plus faciles à tester.
Quelques habitudes donnent des API qui vieillissent bien :
- Modélisez des états, pas des booléens.
isLoading,isErroretisEmptysous forme de flags séparés autorisent des combinaisons impossibles. Une seule unionstatus, non. - Nommez les variantes selon l’intention.
variant="danger"survit à un changement d’identité visuelle ;color="red", non. - Acceptez des children ou des slots pour le contenu. Un composant qui prend des props
title,subtitle,iconetfooterTextne cessera de grossir ; un composant qui accepte du contenu composé, non. - Laissez passer les attributs natifs. Un
Buttondoit acceptertype,disabledetaria-*sans que chacun soit redéclaré.
type Status = 'loading' | 'empty' | 'error' | 'ready';
interface StatCardProps {
/** Libellé court affiché au-dessus de la valeur. */
label: string;
/** Valeur formatée ; ignorée sauf si status vaut 'ready'. */
value?: string;
status: Status;
}
Le contexte et les stores globaux gardent leur place : thème, langue, utilisateur courant. Ces données sont réellement ambiantes. La plupart des autres données devraient arriver par les props, car un composant dont toutes les entrées sont visibles dans sa signature peut être rendu n’importe où, y compris dans un test.
Tester les composants
Les tests unitaires vérifient que les composants s’affichent correctement avec des props données et gèrent les interactions utilisateur comme prévu. Les tests d’intégration valident que les composants assemblés fonctionnent correctement ensemble. Appliquez le principe directeur de Testing Library : testez le comportement, pas les détails d’implémentation.
Concrètement, cela signifie interroger le DOM selon ce que perçoit l’utilisateur (rôle, nom accessible, texte visible) plutôt que selon des noms de classes ou un état interne :
test('shows the error message and a retry button', async () => {
render(InvoiceTable, { status: 'error', message: 'Could not load invoices.' });
expect(screen.getByText('Could not load invoices.')).toBeVisible();
expect(screen.getByRole('button', { name: 'Retry' })).toBeEnabled();
});
Un test écrit ainsi survit à une refonte du balisage et sert en plus de vérification d’accessibilité légère : si vous ne trouvez pas le bouton par son rôle et son nom, un lecteur d’écran ne le trouvera pas non plus. La logique pure, comme la fonction qui associe une variante à des noms de classes, se teste le plus simplement comme une fonction ordinaire, sans aucun rendu.
Documenter au fil de la construction
L’isolation vous offre la documentation presque gratuitement, à condition de la consigner tant que les décisions sont fraîches. Pour chaque composant, notez :
- À quoi il sert et, tout aussi important, à quoi il ne sert pas.
- Chaque prop, avec son type, sa valeur par défaut et une description d’une ligne.
- Les états qu’il prend en charge et l’apparence de chacun.
- Le comportement au clavier et les attributs ARIA qu’il gère pour le compte de l’utilisateur du composant.
Placez les descriptions dans les définitions de types elles-mêmes, comme dans l’exemple StatCardProps ci-dessus : les éditeurs les affichent au survol et elles ne peuvent pas s’éloigner du code.
Les pièges à éviter
Le développement orienté composants a des modes d’échec qui méritent d’être nommés :
- L’abstraction prématurée. Extrayez un composant partagé lorsque vous avez deux ou trois usages réels, pas un seul usage futur imaginé.
- L’explosion des props. Quand un composant a plus de props booléennes que de lignes de balisage, découpez-le en morceaux plus petits.
- L’isolation sans intégration. Chaque composant peut sembler parfait dans sa propre story alors que la page casse quand même. Conservez aussi un petit nombre de tests au niveau des pages.
Conclusion
Le développement orienté composants produit des interfaces plus cohérentes, plus faciles à maintenir et plus rapides à construire. Partez des plus petites briques, donnez à chacune une API explicite et un ensemble complet d’états, puis assemblez-les progressivement.