Écrire des composants réutilisables
Introduction
Les chapitres précédents de ce livre vous ont, je l’espère, permis de découvrir React d’une façon aussi pratique que possible. Le but était de ne pas surcharger votre apprentissage de plein de notions théoriques avant que vous ne soyez capables de créer une application par vous-même.
À présent que les bases de React sont acquises (si vous êtes arrivés jusqu’ici cela ne fait aucun doute), nous allons revenir sur la première notion que nous avons vue : les composants. Depuis que nous les avons découverts au premier chapitre, nous avons trouvé toute sorte de moyens de les faire communiquer entre eux et de les organiser au sein de l’application. À présent, voyons plus en détail comment les rendre plus robustes et réutilisables.
Nous commencerons par voir brièvement les grands principes d’écriture de composants, puis nous verrons ce que React et les années de pratique de la communauté nous offrent pour arriver à ces fins.
Ce chapitre sera un brin plus théorique que les précédents : pas de cas pratique construit au fil du chapitre, mais plutôt un ensemble de petits exemples associés à chaque notion développée. Ceci a pour conséquence que les sous-sections de ce chapitre peuvent être lues dans n’importe...
Principes pour des composants réutilisables
Qu’entend-on par composants réutilisables ? Tout d’abord, il est important de préciser que le but n’est pas forcément de diffuser vos composants à la communauté, ni même de forcément les réutiliser dans un autre projet. Mais développer vos composants comme s’ils allaient être utilisés dans une autre application et par une autre personne les rendra forcément plus faciles à utiliser et à maintenir par vous-même. Ce n’est d’ailleurs pas propre aux composants React, cette pratique est courante pour écrire du code de qualité en général.
Quels sont donc les principes à respecter au maximum pour écrire des composants de qualité ? Ou plutôt : quelles sont les bonnes pratiques de développement qui sont particulièrement applicables aux composants ?
1. Des composants aussi simples que possible
Développer une application complexe pourra vous inciter à écrire des composants complexes, mais c’est pourtant dans ce cas qu’il sera plus pertinent d’écrire de petits composants, même si ce n’est pas pour les réutiliser.
Par exemple, un composant d’affichage de fiche contact pourrait se présenter ainsi dans une première version :
const ContactView = ({ contact, editGeneralInfos, editAddress }) => (
<div>
<section>
<h2>General info</h2>
<p>First name: {contact.firstName}</p>
<p>Last name: {contact.lastName}</p>
{/* ... */}
<button onClick={() => editGeneralInfos()}>Edit</button>
</section>
<section>
<h2>Address</h2>
<p>
{contact.address.addressLineOne}
<br />
{contact.address.addressLineTwo}
<br />
{/* ... */}
</p> ...
Les high-order components
Les composants de haut niveau, plus couramment appelés high-order components ou HOC, sont un moyen particulièrement élégant de créer des composants réutilisables. Le principe est simple : un HOC est une fonction qui prend en paramètre une définition de composant (classe ou fonction), et renvoie une nouvelle définition de composant, qui ajoute du comportement à la première. Il s’agit en fait du pattern Décorateur appliqué aux composants React.
Vous en avez déjà utilisé sans même le savoir. La fonction connect de React-Redux est un HOC. En effet, en appelant connect(mapStateToProps), vous obtenez une fonction prenant un composant en paramètre (celui que vous avez défini à côté), et renvoyant un composant, connecté à Redux. Autres exemples : les fonctions withRouter de React Router (chapitre Gestion de formulaires et du routage), ou encore withNavigation de React Navigation (cf. chapitre Développer pour le mobile avec React Native).
Voici un exemple très simple de HOC :
const addBorder = borderWidth => Component => props => (
<div style={{
borderColor: 'black', borderStyle: 'solid', borderWidth
}}>
<Component {...props} />
</div>
)
const MyText = <p>Hello!</p>
const MyTextWithBorder = addBorder(5)(MyText)
Vous obtenez un composant MyTextWithBorder qui affiche le texte « Hello » avec une bordure de 5 pixels. Ici, addBorder est donc bien ce que l’on appelle un high-order component.
Quel est l’intérêt d’un HOC ? Un pattern très utile est d’extraire un comportement partagé par plusieurs composants dans des fonctions réutilisables. Voyons un exemple plus fourni.
1. Exemple : champ de saisie d’un numéro de téléphone
Pour appréhender les HOC, nous allons dans cette section utiliser le concept pour créer un champ de saisie de numéro de téléphone, qui :
-
n’acceptera que les chiffres, parenthèses, tirets et espaces en entrée (à la frappe) ;
-
mettra en forme le numéro de téléphone lorsque...
Les render props
Dans la section précédente, nous avons vu comment les high-order components pouvaient répondre au besoin de mutualiser de la logique commune à plusieurs composants. Dans certains cas d’utilisation, une manière alternative de répondre à ce besoin est d’utiliser des render props (terme qui ne trouve pas de traduction française satisfaisante).
Comme pour les HOC, le principe de base est très simple : donner à un composant une ou plusieurs propriétés qui sont des fonctions permettant de générer le rendu d’un composant. Voyons plus concrètement avec un exemple à quoi cela ressemble :
const ContactView = props => {
const { contact, renderPhone } = props
return (
<div>
<span>{contact.name}</span>
<span>{renderPhone(contact.phone)}</span>
</div>
)
}
ContactView.propTypes = {
contact: PropTypes.shape({
name: PropTypes.string,
phone: PropTypes.string
}),
renderPhone: PropTypes.func
}
ContactView.defaultProps = {
renderPhone: phone => phone
}
Ici, le composant ContactView ne fait qu’afficher le nom d’une personne et son numéro de téléphone, mais vous remarquerez qu’on a la possibilité de lui passer en propriété une fonction renderPhone permettant de personnaliser l’affichage du numéro de téléphone. Grâce aux defaultProps du composant, le comportement par défaut sera simplement d’afficher le numéro, mais nous pouvons également utiliser notre composant ainsi :
<ContactView
contact={{ name: 'Peter', phone: '514-555-1234' }}
renderPhone={phone => <a href={`tel:${phone}`}>{phone}</a>}
/>
Ainsi, dans notre fiche contact, nous aurons un lien cliquable permettant de lancer un appel téléphonique....
Les hooks
Les high-order components et les render props permettent chacun à leur manière de répondre à un même besoin : faire en sorte qu’un composant soit le plus réutilisable possible, notamment en séparant ce qui peut être mutualisé (comportement, affichage de base…) de ce qui doit laisser un certain degré de liberté à l’utilisateur du composant (affichage personnalisé…).
Mais nous avons également vu dans le deuxième chapitre de ce livre que React propose une fonctionnalité qui pourrait également répondre à ce besoin, je parle naturellement des hooks. Nous avons vu comment utiliser certains des hooks standards de React (useState, useCallback, useEffect…), ou encore des hooks apportés par des bibliothèques tierces (useSelector de React Redux, useQuery de Apollo Client), mais bonne nouvelle : on peut également écrire nos propres hooks !
Reprenons par exemple ce que nous avions fait dans la section sur les render props : avoir un composant capable d’afficher la taille actuelle de la fenêtre. Il s’agissait de s’abonner à l’évènement resize de la fenêtre et de conserver cette taille dans un état interne au composant.
En utilisant les hooks, nous nous abonnerons à l’évènement dans useEffect, et nous...
Les contextes et le pattern Provider/Consumer
Dans la dernière section de ce chapitre nous allons voir un pattern que vous avez déjà utilisé sans forcément le savoir au fil de ce livre, il s’agit du pattern Provider/Consumer. Le but est de partager des informations entre les composants sans que celles-ci ne soient passées par les propriétés. Cela vous rappelle quelque chose ? C’est ce qui avait motivé l’utilisation de Redux dans le chapitre Concevoir une application avec Redux.
Aujourd’hui, React 16 fournit un outil permettant de mettre en place une gestion d’informations partagées sans avoir besoin de passer par Redux, ce qui peut être très pratique pour de petites applications, ne nécessitant pas tous les apports de Redux, comme des actions asynchrones. Cette fonctionnalité, ce sont les contextes.
Commençons par imaginer la situation suivante : notre application doit afficher divers composants graphiques avec une charte de couleurs précises (un arrière-plan et une couleur pour le texte). Nous souhaitons que les composants aient accès à ces couleurs sans passer un objet contenant ces couleurs en propriété dans toute la hiérarchie des composants.
On peut également imaginer que nous souhaitons que cette gestion de thèmes puisse être réutilisée dans plusieurs applications, qui n’utiliseraient pas forcément Redux, d’où l’intérêt de passer par une fonctionnalité standard de React.
À l’usage, cela pourrait donner ceci pour un composant Box :
// Box.js
import React from 'react'
import { injectTheme } from './theme'
const Box = ({ backgroundColor, textColor, text }) => {
const style = {
backgroundColor: backgroundColor,
color: textColor
}
return <div style={style}>{text}</div>
}
export default injectTheme(Box)
// Utilisation dans un autre fichier:
<Box text="Hello!"/>
Lorsqu’on utilise le composant Box, on ne lui passe donc pas les propriétés backgroundColor et textColor, mais le composant a bien accès à ces valeurs...
Conclusion
Créer des composants au maximum réutilisables et donc génériques est un besoin auquel est confronté tout développeur, que ce soit en React ou non. Il est fréquent que dans un projet on ait d’un côté les composants représentant les fonctionnalités d’une application (les pages ou écrans), et d’un autre, une bibliothèque de composants partagés, qui peuvent être utilisés par plusieurs fonctionnalités. Pour de gros projets, il est même courant que des composants soient partagés entre plusieurs applications, par exemple pour conserver une homogénéité graphique ou comportementale.
Dans ce chapitre, après avoir énoncé quelques principes bons à garder en tête au moment de développer de nouveaux composants, nous avons vu trois moyens de mettre cela en œuvre. Les high-order components et les render props ont largement fait leurs preuves, et si généralement ils répondent au même besoin, l’utilisation de l’un ou de l’autre est souvent une préférence du développeur. Certaines bibliothèques ou applications sont organisées autour de HOC, d’autres autour de render props. Quant aux hooks, ils semblent bien faire l’unanimité malgré leur jeunesse, c’est...