
Render Props - Comprendre le pattern (moins courant avec les hooks)
Plongez dans le pattern render props de React. Apprenez comment partager du code entre composants en passant une fonction comme prop, ses avantages, inconvénients et sa relation avec les hooks modernes.
Définition et principe des Render Props
Similaire aux HOCs dans son objectif de partager la logique stateful, le pattern Render Props offre une approche différente pour atteindre la réutilisabilité du code dans React, particulièrement populaire avant l'avènement des Hooks. Ce pattern repose sur l'idée simple mais puissante d'utiliser une prop dont la valeur est une fonction. Le composant qui reçoit cette fonction l'utilise pour savoir quoi rendre, déléguant ainsi une partie de sa logique de rendu au composant parent.
Le coeur du pattern consiste pour un composant (appelons-le le fournisseur de logique) à invoquer une fonction passée via ses props, en lui transmettant éventuellement des données (son état interne, des valeurs calculées, etc.) comme arguments. La fonction, définie par le composant consommateur, retourne alors un élément React qui sera intégré dans le rendu final. Le nom de la prop contenant cette fonction est souvent `render` par convention, d'où le nom du pattern, mais techniquement, n'importe quel nom de prop peut être utilisé pour passer cette fonction.
Une variante très courante de ce pattern n'utilise pas une prop nommée `render`, mais exploite plutôt la prop spéciale `children`. Au lieu de passer des éléments JSX comme enfants, on passe une fonction. Le composant fournisseur de logique invoque alors `this.props.children()` (en classe) ou `props.children()` (en fonctionnel) en lui passant les données nécessaires. Cela permet une syntaxe souvent perçue comme plus intégrée et déclarative dans le JSX.
Cas d'usage et exemple concret
Les Render Props sont particulièrement utiles pour encapsuler un comportement ou un état qui doit être partagé, tout en laissant le contrôle total sur le rendu au composant consommateur. Des exemples typiques incluent le suivi de la position de la souris, la gestion de l'état d'un élément d'interface (comme un interrupteur on/off, une modale), l'abonnement à des sources de données externes, ou encore l'abstraction de requêtes API dont les résultats sont passés à la fonction de rendu.
Considérons un composant `MouseTracker` qui suit les coordonnées X et Y de la souris à l'intérieur de sa zone et les rend disponibles via une render prop.
import React, { useState } from 'react';
// Composant fournisseur de logique (MouseTracker)
function MouseTracker({ render }) { // Prend une prop 'render'
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (event) => {
setPosition({
x: event.clientX,
y: event.clientY,
});
};
return (
{/* Invoque la fonction passée via la prop 'render',
en lui passant l'état actuel (la position) */}
{render(position)}
);
}
// Composant consommateur
function App() {
return (
Bougez la souris !
(
// La fonction définit ce qui doit être rendu
La position actuelle est ({mousePosition.x}, {mousePosition.y})
)} />
);
}
export default App;Ici, `MouseTracker` gère la logique de capture des mouvements de souris et stocke la position dans son état. Au lieu de décider lui-même comment afficher ces coordonnées, il appelle la fonction reçue dans la prop `render` et lui passe l'objet `position`. Le composant `App`, lui, définit cette fonction de rendu pour afficher les coordonnées dans un paragraphe. La logique est partagée, mais le rendu est contrôlé par le consommateur.
La même chose peut être réalisée en utilisant `children` comme fonction :
// MouseTracker (légèrement modifié)
function MouseTracker({ children }) { // Prend 'children' au lieu de 'render'
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
return (
{/* Invoque la fonction passée comme enfant */}
{children(position)}
);
}
// App (utilisation avec children as a function)
function App() {
return (
Bougez la souris ici aussi !
{mousePosition => (
La position via children est ({mousePosition.x}, {mousePosition.y})
)}
);
}Cette syntaxe avec `children` est souvent préférée car elle s'intègre naturellement dans la structure JSX.
Avantages et inconvénients des Render Props
L'un des grands avantages des Render Props est l'explicité. Contrairement aux HOCs où les props injectées peuvent sembler venir de nulle part, avec les Render Props, la source des données (les arguments passés à la fonction) est clairement visible dans le code du composant consommateur. Ce pattern évite également le problème du "wrapper hell" des HOCs, car il n'introduit pas de couches d'imbrication supplémentaires dans l'arbre des composants. Il offre une grande flexibilité, permettant au consommateur de rendre absolument n'importe quoi en utilisant les données fournies.
Cependant, l'utilisation intensive de Render Props, surtout lorsqu'ils sont imbriqués (un composant utilisant une render prop à l'intérieur d'une autre render prop), peut conduire à une structure JSX profondément indentée et potentiellement difficile à lire, parfois surnommée le "Pyramid of Doom" ou comparée au "callback hell" en JavaScript asynchrone. Bien que moins problématique que le "wrapper hell" pour le débogage de l'arbre, cette complexité visuelle peut être un inconvénient. De plus, pour des cas simples, la syntaxe peut sembler un peu verbeuse comparée à d'autres approches.
Par rapport aux HOCs, les Render Props résolvent le problème de la collision de noms de props de manière plus élégante, car le consommateur contrôle explicitement les noms des variables recevant les données (via les paramètres de la fonction). Ils sont généralement considérés comme plus flexibles et plus explicites que les HOCs.
Render Props face aux Hooks
Tout comme pour les HOCs, l'arrivée des Hooks a largement supplanté le besoin des Render Props pour de nombreux cas d'usage. Les hooks personnalisés offrent une manière beaucoup plus directe et moins verbeuse de partager la logique stateful sans nécessiter de passer des fonctions via les props ni de créer des imbrications complexes dans le JSX.
Reprenons l'exemple du `MouseTracker`. Sa logique peut être entièrement encapsulée dans un hook personnalisé `useMousePosition` :
import React, { useState, useEffect } from 'react';
// Hook personnalisé
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
// Nettoyage de l'effet
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []); // Tableau de dépendances vide pour exécuter au montage/démontage
return position;
}
// Utilisation du hook dans App
function App() {
const mousePosition = useMousePosition(); // Appel direct du hook
return (
La position via le hook est : ({mousePosition.x}, {mousePosition.y})
Plus besoin de composant wrapper ou de render prop !
);
}
export default App;Comme on peut le voir, l'utilisation du hook `useMousePosition` est beaucoup plus concise et directe. La logique est réutilisable, et le composant `App` reste simple et focalisé sur son propre rendu, sans la structure `
En conclusion, bien que les Render Props soient devenus moins courants pour écrire du nouveau code grâce à la puissance et la simplicité des Hooks, leur compréhension reste précieuse. C'est un pattern fondamental dans l'histoire de la composition React, et vous pourriez le rencontrer dans des projets existants ou certaines bibliothèques. Connaître son fonctionnement aide à apprécier l'évolution des techniques de composition dans React et à mieux comprendre les solutions apportées par les Hooks.