Maîtrise les motifs avancés de composants React incluant les composants composés, les render props, les composants d'ordre supérieur et le motif provider pour construire des architectures UI flexibles et réutilisables.
Au fur et à mesure que tes applications React grandissent, tu commences à remarquer des problèmes récurrents : Comment partager la logique entre les composants ? Comment construire des composants flexibles qui fonctionnent dans de nombreux contextes ? Comment maintenir les composants faiblement couplés ?
Les motifs de conception sont des solutions éprouvées à ces problèmes récurrents. Dans React, plusieurs motifs ont émergé au fil des années, chacun répondant à un défi spécifique :
| Motif | Idéal Pour |
|---|---|
| Composants Composés | Des composants qui fonctionnent ensemble en groupe (onglets, accordéons, listes déroulantes) |
| Render Props | Partager la logique avec état tout en gardant le contrôle total sur le rendu |
| Composants d'Ordre Supérieur | Encapsuler des composants pour injecter un comportement supplémentaire |
| Hooks Personnalisés | Partager la logique avec état de la façon la plus simple (l'approche moderne) |
| Motif Provider | Rendre les données disponibles à un arbre de composants profond sans prop drilling |
Aucun motif n'est "le meilleur" — chacun résout un problème différent. Comprendre tous ces motifs t'aide à choisir le bon outil pour chaque situation et à comprendre les bases de code existantes qui utilisent des motifs plus anciens.
Nous allons explorer chacun d'eux avec des exemples pratiques.
Le motif des composants composés te permet de créer un ensemble de composants qui fonctionnent ensemble pour former un élément UI complet, tout en donnant au consommateur le contrôle total sur la structure et l'ordre.
Pense aux balises HTML natives <select> et <option> :
<select>
<option value="a">Option A</option>
<option value="b">Option B</option>
</select>Le <select> gère l'état (quelle option est sélectionnée), et chaque <option> lit cet état partagé. Ils n'ont de sens qu'ensemble — c'est un composant composé.
Voici comment construire un composant composé Tabs en React :
const TabsContext = React.createContext();
function Tabs({ children, defaultIndex = 0 }) {
const [activeIndex, setActiveIndex] = React.useState(defaultIndex);
return (
<TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
<div>{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }) {
return <div role="tablist">{children}</div>;
}
function Tab({ index, children }) {
const { activeIndex, setActiveIndex } = React.useContext(TabsContext);
const isActive = index === activeIndex;
return (
<button
role="tab"
className={isActive ? 'active' : ''}
onClick={() => setActiveIndex(index)}
>
{children}
</button>
);
}
function TabPanel({ index, children }) {
const { activeIndex } = React.useContext(TabsContext);
if (index !== activeIndex) return null;
return <div role="tabpanel">{children}</div>;
}Utilisation :
<Tabs>
<TabList>
<Tab index={0}>Profil</Tab>
<Tab index={1}>Paramètres</Tab>
</TabList>
<TabPanel index={0}><p>Contenu du profil</p></TabPanel>
<TabPanel index={1}><p>Contenu des paramètres</p></TabPanel>
</Tabs>Le parent Tabs gère l'état via le contexte, et les enfants le consomment. Le consommateur contrôle la mise en page et l'ordre tandis que le composant composé gère le comportement.
Render Props est un motif où un composant reçoit une fonction comme prop (ou comme children) et l'appelle avec des données :
function MouseTracker({ render }) {
const [position, setPosition] = React.useState({ x: 0, y: 0 });
React.useEffect(() => {
const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return render(position);
}
// Utilisation
<MouseTracker render={({ x, y }) => (
<p>La souris est à ({x}, {y})</p>}
/>Le MouseTracker gère la logique (suivi de la position de la souris) et laisse le consommateur décider comment la rendre. Tu peux aussi utiliser children comme fonction :
<MouseTracker>
{({ x, y }) => <p>La souris est à ({x}, {y})</p>}
</MouseTracker>Les Composants d'Ordre Supérieur (HOC) sont des fonctions qui prennent un composant et retournent un nouveau composant avec un comportement supplémentaire :
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const user = useAuth();
if (!user) return <p>Merci de te connecter</p>;
return <WrappedComponent {...props} user={user} />;
};
}
const ProtectedDashboard = withAuth(Dashboard);Les HOC étaient très populaires avant l'arrivée des hooks. Ils suivent une convention de nommage : withSomething. Des exemples courants incluent withRouter de React Router (v5) et connect de Redux.
Quand utiliser quoi :
Le motif provider utilise le Context de React pour rendre les données disponibles à tout un sous-arbre sans passer les props à travers chaque niveau :
const ThemeContext = React.createContext('light');
function ThemeProvider({ children }) {
const [theme, setTheme] = React.useState('light');
const toggle = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
// Hook personnalisé pour consommer le contexte
function useTheme() {
const context = React.useContext(ThemeContext);
if (!context) {
throw new Error('useTheme doit être utilisé dans un ThemeProvider');
}
return context;
}C'est le motif moderne recommandé pour la plupart des besoins de partage d'état. Le provider encapsule la logique d'état, et le hook personnalisé fournit une API propre pour les consommateurs :
function Header() {
const { theme, toggle } = useTheme();
return (
<header style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
<button onClick={toggle}>Basculer le thème</button>
</header>
);
}
function App() {
return (
<ThemeProvider>
<Header />
<Main />
</ThemeProvider>
);
}Les hooks personnalisés sont la façon la plus moderne et recommandée de partager la logique. Ce sont juste des fonctions qui utilisent des hooks React :
function useWindowSize() {
const [size, setSize] = React.useState({
width: window.innerWidth,
height: window.innerHeight
});
React.useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}Les hooks personnalisés ont remplacé les render props et les HOC comme motif principal pour la réutilisation de code car ils sont plus simples, composables, et ne produisent aucun composant supplémentaire dans l'arbre.
Le motif contrôlé vs non contrôlé détermine qui possède l'état — le composant lui-même ou son parent.
Composant non contrôlé — gère son propre état :
function Toggle() {
const [isOn, setIsOn] = React.useState(false);
return (
<button onClick={() => setIsOn(!isOn)}>
{isOn ? 'ACTIVÉ' : 'DÉSACTIVÉ'}
</button>
);
}
// Utilisation — tu n'as aucun contrôle sur l'état
<Toggle />Composant contrôlé — l'état est géré par le parent :
function Toggle({ isOn, onToggle }) {
return (
<button onClick={onToggle}>
{isOn ? 'ACTIVÉ' : 'DÉSACTIVÉ'}
</button>
);
}
// Utilisation — le parent contrôle l'état
const [isOn, setIsOn] = useState(false);
<Toggle isOn={isOn} onToggle={() => setIsOn(!isOn)} />Composant flexible — supporte les deux modes :
function Toggle({ isOn: controlledIsOn, onToggle, defaultOn = false }) {
const [internalIsOn, setInternalIsOn] = React.useState(defaultOn);
const isControlled = controlledIsOn !== undefined;
const isOn = isControlled ? controlledIsOn : internalIsOn;
const handleToggle = () => {
if (!isControlled) setInternalIsOn(!isOn);
onToggle?.(!isOn);
};
return (
<button onClick={handleToggle}>
{isOn ? 'ACTIVÉ' : 'DÉSACTIVÉ'}
</button>
);
}Ce motif double mode est utilisé par de nombreuses bibliothèques UI. Les éléments <input> natifs dans React suivent la même idée : passer value le rend contrôlé, tandis qu'utiliser defaultValue le laisse non contrôlé.
Règle générale : Rends les composants contrôlés quand le parent a besoin de synchroniser ou valider l'état. Utilise non contrôlé quand le composant est autonome.
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const TabsContext = React.createContext();
function Tabs({ children, defaultIndex = 0 }) {
const [activeIndex, setActiveIndex] = React.useState(defaultIndex);
return (
<TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
<div id="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }) {
return <div role="tablist" style={{ display: "flex", gap: "4px" }}>{children}</div>;
}
function Tab({ index, children }) {
const { activeIndex, setActiveIndex } = React.useContext(TabsContext);
return (
<button
className={index === activeIndex ? "active" : ""}
onClick={() => setActiveIndex(index)}
style={{
padding: "8px 16px",
background: index === activeIndex ? "#646cff" : "#333",
color: "#fff",
border: "none",
borderRadius: "4px 4px 0 0",
cursor: "pointer"
}}
>
{children}
</button>
);
}
function TabPanel({ index, children }) {
const { activeIndex } = React.useContext(TabsContext);
if (index !== activeIndex) return null;
return <div id="active-panel" style={{ padding: "16px", background: "#1a1a2e", borderRadius: "0 4px 4px 4px" }}>{children}</div>;
}
function App() {
return (
<Tabs>
<TabList>
<Tab index={0}>Onglet 1</Tab>
<Tab index={1}>Onglet 2</Tab>
</TabList>
<TabPanel index={0}>Contenu 1</TabPanel>
<TabPanel index={1}>Contenu 2</TabPanel>
</Tabs>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>Quel motif a largement remplacé les render props et les HOC pour partager la logique avec état ?