Vincent Barrault

Faire de la projection de contenu avec React

Faire de la projection de contenu avec React

Table des matières

Introduction

La projection de contenu, c'est le fait de devoir déplacer du contenu depuis un composant parent vers un composant enfant.

La plupart du temps, il n'y a qu'un seul contenu à déplacer, ce qui fait que les choses sont simple.

Mais parfois, j'ai besoin de "remplir" plusieurs "emplacements" de mon composant, comme par exemple avec une Card:

<div class="card">
  <div class="header">
    <!-- Title -->
  </div>
  <div class="content">
    <!-- Content -->
  </div>
  <div class="footer">
    <!-- Actions -->
  </div>
</div>

Les 3 emplacements sont ici: Title, Content et Footer.

Dans Angular, il existe quelque chose qui répond exactement à mon besoin: ng-content et l'attribut select:

<div class="card">
  <div class="header">
    <ng-content select=".header"></ng-content>
  </div>
  <div class="content">
    <ng-content select=".content"></ng-content>
  </div>
  <div class="footer">
    <ng-content select=".footer"></ng-content>
  </div>
</div>

Ce composant peut s'utiliser de la manière suivante:

<my-card>
  <div class="header">
    <h3>title</h3>
  </div>
  <div class="content">
    <p>Lorem ipsum</p>
  </div>
  <div class="actions">
    <button>Click me</button>
  </div>
</my-card>

Mais pour React, il n'y a qu'un seul children et il est impossible pour moi de "ranger" les differents enfants dans des emplacements spécifiques.

Les propriétés

Plutôt que de passer naturelement par la propriété children, je vais créer une propriété pour chaque emplacements.

export const MyCard = ({ header, content, footer }) => (
  <div className="card">
    <div className="header">{header}</div>
    <div className="content">{content}</div>
    <div className="footer">{footer}</div>
  </div>
);

Et comme il est possible de mettre un template dans une propriété, voici comment ce composant pourra etre utilisé:

<MyCard
  header={<h3>title</h3>}
  content={<p>Lorem ipsum</p>}
  footer={<button>Click me</button>}
/>

Mais je trouve que cela casse la structure du HTML et que le code pourrait vite devenir difficile à maintenir.

Filtrer les children

Je me rend compte que passer par children reste la meilleur option pour la lisibilité, il faut donc améliorer le rendu de ce dernier pour "dispatcher" les enfants aux bons endroits.

L'api React me permet de boucler sur les composants passés dans les children et de créer des filtres.

https://react.dev/reference/react/Children#children-foreach

Si je créer une fonction filterChildren qui retourne uniquement les enfants dont j'ai besoin, je vais pouvoir organiser mon code comme ceci:

export const MyCard = ({ children }) => (
  <div className="card">
    <div className="header">{filterChildren(children, "header")}</div>
    <div className="content">{filterChildren(children, "content")}</div>
    <div className="footer">{filterChildren(children, "footer")}</div>
  </div>
);

Et l'utilisation de ce composant pourrait ressembler à:

<MyCard>
  <MyCard.header>
    <h3>title</h3>
  </MyCard.header>
  <MyCard.content>
    <p>Lorem ipsum</p>
  </MyCard.content>
  <MyCard.footer>
    <button>Click me</button>
  </MyCard.footer>
</MyCard>

Je trouve après coup que cela compléxifie un peu le composant Card et que le filterChildren n'est pas forcement la meilleur des solutions.

Séparer la structure

Une autre approche, peut être plus simple, est de séparer le composant en plusieurs composants

export const MyCard = ({ children }) => {
  return <div className="card">{children}</div>;
};
export const MyCardHeader = ({ children }) => {
  return <div className="header">{children}</div>;
};
export const MyCardContent = ({ children }) => {
  return <div className="content">{children}</div>;
};
export const MyCardFooter = ({ children }) => {
  return <div className="footer">{children}</div>;
};

L'utilisation de cette solution ressemblerai à ceci:

<MyCard>
  <MyCardHeader>
    <h3>title</h3>
  </MyCardHeader>
  <MyCardContent>
    <p>Lorem ipsum</p>
  </MyCardContent>
  <MyCardFooter>
    <button>Click me</button>
  </MyCardFooter>
</MyCard>

La structure de MyCard n'est plus décidé par MyCard lui même mais par son utilisateur

Le composant devient uniquement un container qui sert à afficher sa bordure, son fond et ses marges

Le code de MyCard devient plus simple, mais son utilisation est plus libre et laisse la possibilité de faire plus d'erreurs

Conclusion

Je ne pense pas qu'il y ai de meilleur solution parmi les 3 que j'ai découvertes.

Cela dépend surtout de l'endroit ou l'on veut placer la responsabilité et la complexité.