Avancé30 min de lecture

Motifs Avancés

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.

Pourquoi les Motifs Sont Importants

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 :

MotifIdéal Pour
Composants ComposésDes composants qui fonctionnent ensemble en groupe (onglets, accordéons, listes déroulantes)
Render PropsPartager la logique avec état tout en gardant le contrôle total sur le rendu
Composants d'Ordre SupérieurEncapsuler des composants pour injecter un comportement supplémentaire
Hooks PersonnalisésPartager la logique avec état de la façon la plus simple (l'approche moderne)
Motif ProviderRendre 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.

Motif des Composants Composés

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> :

html
<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 :

jsx
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 :

jsx
<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 et Composants d'Ordre Supérieur

Render Props est un motif où un composant reçoit une fonction comme prop (ou comme children) et l'appelle avec des données :

jsx
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 :

jsx
<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 :

jsx
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 :

  • Les render props donnent au consommateur une flexibilité maximale sur le rendu.
  • Les HOC sont bons pour les préoccupations transversales (vérifications d'authentification, journalisation) mais peuvent mener à un "enfer d'encapsulation" — de nombreux HOC imbriqués deviennent difficiles à déboguer.
  • Les hooks personnalisés ont largement remplacé ces deux motifs pour partager la logique avec état. Privilégie les hooks quand c'est possible.

Le Motif Provider et les Hooks Personnalisés

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 :

jsx
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 :

jsx
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 :

jsx
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.

Motif Contrôlé vs Non Contrôlé

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 :

jsx
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 :

jsx
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 :

jsx
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.

Composant Tabs Composé

html
<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 ?

Prêt à pratiquer ?

Crée ton compte gratuit pour accéder à l'éditeur de code interactif, lancer les défis et suivre ta progression.