Il y a très peu de règles ESLint que je trouve pertinentes dans les projets sur lesquels je travaille. Mais l'un des seuls plugins ESLint que j'ajoute systématiquement sur mes projets React, c'est eslint-plugin-react-hooks
.
Les hooks sont arrivés dans nos codebase en 2019, et avec eux sont venus un ensemble de règles - un poil atypiques - à suivre pour que React fonctionne comme il le doit.
Ces règles imposent en particulier que les hooks :
- ne soient pas appelés dans des conditions ;
- ne soient pas appelés dans des boucles ;
Ces deux règles ont des conséquences sur la manière dont on écrit et compose nos components. C'est pour nous aider à respecter ces règles que le package eslint-plugin-react-hooks
expose la règle react-hooks/rules-of-hooks
.
L'impact de la Rules of Hooks sur notre code
Imaginons un hook, appelé usePicture(id: string): Picture
. Ce hook prend un id
et retourne un object Picture
associé à cet identifiant.
Maintenant, imaginons que l'on souhaite afficher une liste de Articles
, où le type Article
est défini comme suit :
type Article = {
id: string
title: string
coverPictureId: string | null
}
Un component chargé d'afficher une liste de Article
s'écrit assez facilement :
function ArticleList({ articles } { articles: Article[] }) {
return (
<ol>
{articles.map(article => (
<li key={article.id}>{article.title}</li>
)}
</ol>
)
}
Maintenant, nous souhaiterions afficher une image en utilisant la coverPicture
d'un Article
si celle-ci est définie.
Problème : articles.map
est une boucle, et coverPictureId
est nullable. Impossible alors d'écrire le code suivant :
function ArticleList({ articles } { articles: Article[] }) {
return (
<ol>
{articles.map(article => (
<li key={article.id}>
{article.coverPictureId && (
<img src={usePicture(article.coverPictureId).src} />
)}
{article.title}
</li>
)}
</ol>
)
}
L'usage de usePicture
ici viole à la fois la règle concernant l'itération et la règle concernant les conditions. La seule manière de respecter la Rule of Hooks ici est de créer de nouveaux components dont les responsabilités est d'afficher un item de la liste et la cover picture.
function CoverPicture({ pictureId }: { pictureId: string }) {
const picture = usePicture(pictureId)
return <img src={picture.src} />
}
function ArticleItem({ article }: { article: Article }) {
return (
<li key={article.id}>
{article.coverPictureId && (
<CoverPicture pictureId={article.coverPictureId} />
)}
{article.title}
</li>
)
}
function ArticleList({ articles: Article[] }) {
return (
<ol>
{articles.map(article => (
<ArticleItem key={article.id} article={article} />
)}
</ol>
)
}
Nous avons ici la conséquence principale de l'utilisation des hooks : ils nous poussent a créer des components enfants pour rendre notre code "compatible" avec leurs règles.
Alors, j'aimerais vous partager ici un pattern que j'utilise souvent pour ne pas avoir à créer de components spécifiques à une liste.
Les Getter Components
Un hook ne peut pas être appelé conditionnellement ou dans une itération, mais un component peut.
Il est donc possible d'appeler ce hook inconditionnellement dans un component générique qui expose la valeur du hook par une render prop.
type GetPictureProps = {
pictureId: string,
children: (picture: Picture) => ReactNode
}
function GetPicture({ pictureId, children }: GetPictureProps) {
const picture = usePicture(pictureId)
return children(picture)
}
Il est donc toujours nécessaire de créer un component, mais il s'agit ici d'un component générique que l'on pourra réutiliser partout dans le projet.
Une sorte de contournement d'un hook qui permet d'exposer sa valeur conditionnellement, ou dans une itération, sans violer les règles des hooks.
Et l'on peut alors l'utiliser dans notre component ArticleList
:
function ArticleList({ articles: Article[] }) {
return (
<ol>
{articles.map(article => (
<li key={article.id}>
{article.coverPictureId && (
<GetPicture pictureId={article.coverPictureId}>
{picture => <img src={picture.src} />}
</GetPicture>
)}
{article.title}
</li>
)}
</ol>
)
}
Et cette fois tout est en ordre !
Les hooks ne sont pas nouveaux. Au moment où j'écris cet article, les hooks ont déjà fêté leur 6e anniversaire. Mais ce ne sont pas simplement des fonctions. Les hooks ont leur propre fonctionnement qui parfois - un peu trop à mon goût - tordent notre façon de coder.
Mais en cherchant un peu, on peut bien souvent retomber sur nos pates avec des patterns comme celui-ci.
Happy coding ✌️