frontend / react
Patterns avancés : compound components
Explication
Ce que vous allez apprendre
- Reconnaître les limites d'une API à base de props de configuration pour un composant complexe
- Construire un compound component avec Context pour partager l'état entre sous-composants
- Rattacher des sous-composants au composant principal comme propriétés statiques
- Comparer l'approche Context à l'ancienne technique de clonage d'enfants
- Identifier quand ce pattern apporte une vraie valeur par rapport à une API à props simples
Dans quel contexte ?
Le composant <Onglets configuration={[...]} /> de src/components/Tabs.jsx fonctionnait bien tant que chaque onglet affichait un texte simple. Dès qu'un designer demande un badge de notification sur un onglet précis et une icône personnalisée sur un autre, l'objet configuration explose en options conditionnelles imbriquées, difficile à lire et à maintenir. Réécrire le composant en compound components (<Onglets.Bouton>, <Onglets.Panneau>) redonne à l'appelant la liberté de composer son JSX comme il l'entend.
Étape 1 : le réflexe naturel face à un composant complexe
Face à un composant complexe comme des onglets, un premier réflexe consiste à tout piloter via des props de configuration : une liste d'onglets, un tableau de contenus.
Étape 2 : la limite de cette approche
Cette approche fonctionne un temps, mais dès que les besoins se diversifient, la prop de configuration devient un objet de plus en plus rigide, difficile à faire évoluer.
Étape 3 : une autre approche, les compound components
Les "compound components" proposent autre chose : au lieu d'une seule prop de configuration, on expose plusieurs petits composants qui collaborent implicitement entre eux.
Étape 4 : comment l'utilisateur les assemble
L'utilisateur du composant les assemble librement dans son JSX, comme il assemblerait des balises HTML natives — <select> et ses <option> en sont un exemple natif.
Étape 5 : comment l'état est partagé
Le partage d'état entre les sous-composants se fait via Context : un composant parent fournit la valeur, et chaque sous-composant la consomme avec un hook interne dédié.
Étape 6 : rendre l'API découvrable
Les sous-composants sont ensuite rattachés au composant principal comme des propriétés statiques (Onglets.Bouton), ce qui rend l'API immédiatement découvrable.
Bonne pratique
Onglets.Bouton = OngletsBouton; juste après la définition du composant principal permet à l'IDE de proposer l'auto-complétion dès que l'utilisateur tape Onglets., sans avoir à consulter la documentation.
Une variante plus fragile
Il existe une alternative plus ancienne basée sur le clonage d'enfants, plus fragile car elle casse dès qu'un élément non-composant s'intercale entre les enfants attendus.
| Approche | Flexibilité de composition | Fragilité |
|---|---|---|
| Props de configuration | Faible, objet rigide | Faible |
Clonage d'enfants (cloneElement) | Moyenne | Élevée (casse si un enfant "étranger" s'intercale) |
| Compound components (Context) | Élevée | Faible |
Et ensuite ?
Une fois ce pattern compris, la prochaine étape explore deux patterns plus anciens mais encore présents dans du code existant : les render props et les HOC.
Commandes & code
Compound components
Plusieurs composants qui collaborent implicitement via un état partagé, avec une API déclarative et lisible.
// Objectif d'API : lisible, flexible, sans props "de configuration" qui explosent
// <Onglets>
// <Onglets.Liste>
// <Onglets.Bouton index={0}>Profil</Onglets.Bouton>
// <Onglets.Bouton index={1}>Sécurité</Onglets.Bouton>
// </Onglets.Liste>
// <Onglets.Panneau index={0}>Contenu profil</Onglets.Panneau>
// <Onglets.Panneau index={1}>Contenu sécurité</Onglets.Panneau>
// </Onglets>
const OngletsContext = createContext(null);
function Onglets({ children, indexParDefaut = 0 }) {
const [indexActif, setIndexActif] = useState(indexParDefaut);
return (
<OngletsContext.Provider value={{ indexActif, setIndexActif }}>
<div className="onglets">{children}</div>
</OngletsContext.Provider>
);
}
function useOngletsContext() {
const ctx = useContext(OngletsContext);
if (!ctx) throw new Error("Ce composant doit être utilisé dans <Onglets>");
return ctx;
}
function Liste({ children }) {
return <div role="tablist">{children}</div>;
}
function Bouton({ index, children }) {
const { indexActif, setIndexActif } = useOngletsContext();
return (
<button
role="tab"
aria-selected={indexActif === index}
onClick={() => setIndexActif(index)}
>
{children}
</button>
);
}
function Panneau({ index, children }) {
const { indexActif } = useOngletsContext();
if (indexActif !== index) return null;
return <div role="tabpanel">{children}</div>;
}
// Attachement des sous-composants comme propriétés statiques : l'API du pattern
Onglets.Liste = Liste;
Onglets.Bouton = Bouton;
Onglets.Panneau = Panneau;
export default Onglets;// Utilisation : l'ordre et la composition sont libres, contrairement à une prop "onglets={[...]}"
function ParametresCompte() {
return (
<Onglets indexParDefaut={0}>
<Onglets.Liste>
<Onglets.Bouton index={0}>Profil</Onglets.Bouton>
<Onglets.Bouton index={1}>Sécurité</Onglets.Bouton>
<Onglets.Bouton index={2}>Notifications</Onglets.Bouton>
</Onglets.Liste>
<Onglets.Panneau index={0}><FormulaireProfil /></Onglets.Panneau>
<Onglets.Panneau index={1}><FormulaireSecurite /></Onglets.Panneau>
<Onglets.Panneau index={2}><FormulaireNotifications /></Onglets.Panneau>
</Onglets>
);
}// Variante : partage implicite via clonage d'enfants (moins courant, plus fragile)
function Accordeon({ children }) {
const [ouvertIndex, setOuvertIndex] = useState(null);
return Children.map(children, (enfant, index) =>
cloneElement(enfant, {
estOuvert: index === ouvertIndex,
onToggle: () => setOuvertIndex(index === ouvertIndex ? null : index),
})
);
// fragile : casse si l'utilisateur insère un élément non-composant entre les enfants
// -> Context reste l'approche recommandée pour la plupart des cas
}Résumé
- Les compound components partagent un état implicite via Context, sans props de configuration monolithiques.
- Attacher les sous-composants comme propriétés statiques (
Onglets.Bouton) rend l'API découvrable. - Un hook custom (
useOngletsContext) avec garde d'erreur évite un mauvais usage hors du composant parent. - Le clonage d'enfants (
cloneElement) est une alternative plus fragile, largement supplantée par Context.
Exercices pratiques
Mission : reconstruire les Onglets avant que la config n'explose
Objectif : Migrer un composant piloté par une prop de configuration rigide vers un compound component avec Context.
Contexte
Le composant <Onglets configuration={[{ label: "Profil", contenu: <FormulaireProfil /> }, { label: "Sécurité", contenu: <FormulaireSecurite />, badge: 3 }]} /> doit maintenant afficher un badge de notification uniquement sur l'onglet Sécurité et une icône personnalisée sur l'onglet Notifications. L'objet configuration accumule déjà des champs optionnels conditionnels (badge, icone, disabled) qui rendent chaque nouvelle demande plus fragile à intégrer sans casser les onglets existants.