Dans ce chapitre nous allons parler organisation de composant et voir comment résoudre un problème classique : la personnalisation de composant enfant.
Le problème
Pour comprendre ce que l'on va faire il faut comprendre le problème. On dispose d'un composant capable de rendre une liste d'élément avec un système de recherche. Ce composant prend en paramètre la liste et affiche automatiquement chaque élément.
import {SearchableList} from "./SearchableList.jsx";
const recipes = [
{name: "Feuilles de lasagne", icon: "🍃"},
{name: "Sauce tomate", icon: "🍅"},
{name: "Viande végétalienne hachée", icon: "🌿"},
{name: "Oignon", icon: "🧅"},
{name: "Ail", icon: "🧄"},
{name: "Épinards", icon: "🍃"},
{name: "Tofu", icon: "🥦"},
{name: "Fromage végétalien râpé", icon: "🧀"},
{name: "Sel", icon: "🧂"},
{name: "Poivre", icon: "🌶️"},
{name: "Huile d'olive", icon: "🫒"},
];
function App() {
return (<>
<SearchableList
items={recipes}
/>
</>);
}
Maintenant, dans ce cas précis je souhaite changer la méthode de rendu de chaque élément pour afficher l'icône à droite, ou changer la structure de chaque élément plus en profondeur.
Render props
La première solution à ce problème est de permettre de contrôler le rendu via une propriété de notre composant.
<SearchableList
items={recipes}
itemRenderer={(item, active, baseProps) =>
<li {...baseProps}>
{item.name} {item.icon} {active ? 'x' : ''}
</li>
}
/>
On passe à notre composant une fonction qui sera utilisé pour rendre chaque enfant. Cette fonction est ensuite utilisée dans le composant pour générer le code JSX pour chacun de nos élément.
export function SearchableList({items, itemRenderer = defaultItemRenderer}) {
// ...
return (<div>
<ul className="list-group">
{filteredItems.map((item, k) => (
<Fragment key={item.name}>
{itemRenderer(item, k === selectedItemIndex, {'aria-current': k === selectedItemIndex})}
</Fragment>
))}
</ul>
</div>);
}
On crée aussi une fonction defaultItemRenderer
pour gérer le cas où aucune fonction de rendu n'est passé à notre composant.
Avec cette méthode on a la possibilité, depuis l'extérieur, de contrôler la méthode de rendu d'une partie de notre composant.
Component props
Une autre approche consiste à passer en paramètre le composant à utiliser pour le rendu.
<SearchableList
items={recipes}
itemComponent={ListItemWithIcon}
/>
On crée ensuite notre composant
function ListItemWithIcon ({item, active, ...props}) {
return <li {...props}>
{item.name} {item.icon}
</li>
}
Et dans le code de notre liste on peut utiliser le composant reçu en paramètre en lui donnant une valeur par défaut.
export function SearchableList({items, itemComponent: ItemComponent = ListItem}) {
// ...
return (<div>
<ul className="list-group">
{filteredItems.map((item, k) => (
<Fragment key={item.name}>
<ItemComponent
item={item}
active={k === selectedItemIndex}
aria-active={k === selectedItemIndex}
/>
</Fragment>
))}
</ul>
</div>);
}
Cette approche est un peu moins flexible mais peut s'avérer utile pour par exemple personnaliser l'élément utiliser par un autre.
<Button>Je suis un bouton</Button>
<Button as="a" href="/demo">Bouton lien</Button>
<Button as={Link} to="/demo">Bouton react-router-dom</Button>
Le composant reçu en paramètre est utilisé pour choisir comment rendre le bouton et assurer une bonne sémantique HTML.
import {clsx} from "clsx";
export function Button ({
as: ButtonComponent = 'button',
variant = 'primary',
className,
...props
}) {
// On force un lien si on reçoit un href
if (props.href && ButtonComponent === 'button') {
ButtonComponent = 'a'
}
return <ButtonComponent
className={clsx(`btn btn-${variant}`, className)}
{...props}
/>
}