Concevez des API de composants flexibles grâce aux compound components, au state reducer et au bon choix entre render props et hooks.
Ouvrir cette leçon dans KodokonUn composant <Tabs items={...} labels={...} icons={...} /> finit toujours par crouler sous les props de configuration. Le pattern des compound components renverse l'approche : plusieurs composants collaborent et partagent un état implicite via un contexte, tandis que l'utilisateur garde la main sur la composition JSX (ordre, imbrication, éléments intercalés). C'est le modèle de <select> et <option> en HTML, et celui de bibliothèques comme Radix ou Headless UI.
const TabsContext = createContext(null);
function Tabs({ defaultTab, children }) {
const [active, setActive] = useState(defaultTab);
const value = useMemo(
() => ({ active, setActive }),
[active]
);
return (
<TabsContext.Provider value={value}>
{children}
</TabsContext.Provider>
);
}Chaque sous-composant lit le contexte et n'a besoin d'aucune prop de câblage : l'utilisateur compose librement, le pattern garantit la cohérence. Notez la valeur de contexte enveloppée dans useMemo : sans cela, chaque rendu du parent créerait un nouvel objet et forcerait le rendu de tous les consommateurs.
function Tab({ id, children }) {
const ctx = useContext(TabsContext);
return (
<button
aria-selected={ctx.active === id}
onClick={() => ctx.setActive(id)}
>
{children}
</button>
);
}
// <Tabs defaultTab="a">
// <Tab id="a">Profil</Tab>
// <Tab id="b">Reglages</Tab>
// </Tabs>Deuxième pattern : l'inversion de contrôle, dont la forme la plus aboutie est le state reducer popularisé par Kent C. Dodds dans Downshift. Plutôt que d'ajouter une prop booléenne à chaque nouveau besoin, le hook expose ses transitions d'état : l'appelant fournit un reducer qui reçoit chaque action et peut la laisser passer, la modifier ou l'annuler. La bibliothèque garde son comportement par défaut ; le contrôle fin appartient au consommateur.
function defaultReducer(state, action) {
if (action.type === 'toggle') {
return { on: !state.on };
}
return state;
}
function useToggle({ reducer = defaultReducer } = {}) {
const [state, dispatch] = useReducer(reducer, {
on: false,
});
const toggle = () => dispatch({ type: 'toggle' });
return { on: state.on, toggle };
}Enfin, l'éternel débat render props contre hooks. Les hooks ont gagné pour le partage de logique : pas de fausse hiérarchie dans l'arbre, pas de pyramide de wrappers, composition naturelle de plusieurs sources. Mais la render prop conserve un territoire légitime : quand la variation porte sur le rendu lui-même. Une liste virtualisée qui vous demande comment dessiner chaque ligne, un composant headless qui délègue tout son JSX : là, une fonction de rendu passée en prop reste l'outil le plus direct, car un hook ne peut pas décider où insérer du JSX dans l'arbre de l'appelant.
function MouseTracker({ render }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
const onMove = (e) =>
setPos({ x: e.clientX, y: e.clientY });
return (
<div onMouseMove={onMove}>{render(pos)}</div>
);
}
// Pour la logique seule, un hook suffit :
// const pos = useMousePosition();