Tutoriel vidéo React : Propriétés de rendu

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="https://grafikart.fr/demo">Bouton lien</Button>
<Button as={Link} to="https://grafikart.fr/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}
    />  
}