Conserver les hooks sans en-tête : séparer le comportement des alertes de leur affichage
Découvrez pourquoi un hook React qui retourne du JSX cache une partie de votre arbre de composants, lie le comportement à la présentation, et comment le refactorer pour exposer l’état à la place.
Les hooks personnalisés sont excellents pour cacher les détails d’implémentation, mais parfois ils en cachent trop. Un hook qui renvoie du JSX prêt à l’emploi semble pratique au point d’appel, mais il cache une partie de votre arborescence UI derrière une API qui ressemble à du simple texte, et il lie le comportement à une présentation spécifique. En utilisant un hook d’alertes typique provenant d’un éditeur de tableau de bord, cet article montre comment reconnaître ce problème, comment refactoriser vers un hook sans interface graphique, et quels avantages on retire de ces quelques lignes supplémentaires.
Un hook d’alertes qui affiche sa propre interface
Les alertes conviennent naturellement aux hooks : elles nécessitent un état, éventuellement des temporiseurs, des gestionnaires de fermeture et un mécanisme de file d’attente. Voici comment un tel hook est utilisé :
const {
showSuccess,
showError,
alerts,
} = useAlerts();
return (
<>
{alerts}
<Dashboard />
</>
);
Il existe déjà un indice que quelque chose ne va pas. Les alerts sont directement intégrés dans le JSX, ce qui suggère fortement que l’hook retourne des éléments rendus. L’hook est responsable du comportement, puisqu’il fournit showSuccess et showError, et apparemment aussi de l’apparence des alertes.
L’implémentation le confirme :
const useAlerts = () => {
const [alerts, setAlerts] = useState<Alert[]>([]);
const showSuccess = (message: string) => {
// update state
};
const showError = (message: string) => {
// update state
};
...
return {
showSuccess,
showError,
alerts: (
<AlertContainer>
{alerts.map(({ id, type, message }) => (
<Alert
key={id}
type={type}
message={message}
/>
))}
</AlertContainer>
),
};
};
useAlerts gère l’état des alertes, les fonctions qui le modifient ainsi que le markup : l’encadrement AlertContainer et une Alert par entrée. La frontière entre la logique et la présentation a disparu.
Pourquoi le JSX retourné est trompeur
Cela ressemble à des données
D’un point de vue utilisateur, rien n’indique que alerts est un sous-arbre React. Il se trouve à côté de deux fonctions, porte un nom pluriel suggérant un tableau d’éléments, et pourrait facilement être confondu avec l’état :
const {
showSuccess,
showError,
alerts,
} = useAlerts();
La seule façon de connaître la vérité est d’ouvrir le hook.
Ce dernier cache l’origine de l’interface utilisateur
Un arbre de composants est précieux en partie parce qu’il indique d’où provient chaque élément de l’interface. Lorsqu’on examine le tableau de bord et qu’on voit ceci :
{alerts}
<Dashboard />
on ne peut pas savoir ce que alerts affiche, quels composants il installe, de quel contexte ou de quelles propriétés ces composants dépendent, ni comment ils sont stylisés, sans examiner le hook. Une partie de l’arbre a été déplacée derrière quelque chose qui ressemble à un élément comportemental.
Ce dernier lie deux décisions sans rapport entre elles
Le hook ne peut plus exposer le comportement d’alerte sans également définir le composant d’alerte. Tout écran qui souhaite utiliser la logique d’alerte hérite de son apparence, que cela convienne ou non. Il n’y a aucune raison technique pour que ces deux aspects soient liés.
Refacturation vers un hook sans interface graphique
Le hook doit gérer l’état et les opérations qui s’y rapportent, et rien d’autre. Il faut exposer la liste brute des alertes ainsi que les actions associées, y compris un moyen de fermer une alerte :
const {
alerts,
showSuccess,
showError,
dismissAlert,
} = useAlerts();
Puis afficher les alertes dans le composant, qui est bien là pour cela :
<AlertContainer>
{alerts.map(({ id, type, message }) => (
<Alert
key={id}
type={type}
message={message}
onDismiss={() => dismissAlert(id)}
/>
))}
</AlertContainer>
<Dashboard />
Désormais, l’arborescence des composants est à nouveau claire. Quiconque la consulte voit un AlertContainer, un Alert par entrée identifiée par id, ainsi qu’un gestionnaire de fermeture relié au hook. Le rôle du hook est sans équivoque : il fournit l’état et les actions.
Le code supplémentaire est-il un problème ?
Le composant qui consomme les données devient effectivement plus long, et ce n’est pas un problème. Il est tentant d’affiner une API au point que l’appelant n’ait presque rien à écrire, mais cette perfectionnement peut effacer des informations architecturales utiles. Cinq lignes de code clairement structurées sont plus faciles à maintenir qu’un seul {alerts} cachant toute une partie de l’interface utilisateur.
Même si plusieurs écrans affichent des alertes de manière identique, il est possible d’éviter la répétition sans remettre l’interface utilisateur dans le hook : créez un composant AlertList qui prend alerts et onDismiss en tant que propriétés, ou un composant qui appelle directement le hook. Dans les deux cas, l’affichage se trouve dans un composant, ce qui le rend visible dans l’arborescence et permet de l’analyser, de le mémoriser et de le tester comme n’importe quel autre composant.
La réutilisation devient possible
Avec JSX intégré, la réutilisation du comportement force également la réutilisation de l’interface visuelle, une hypothèse qui ne tient pas toujours. Avec la version sans interface, une zone d’alertes, un bandeau en ligne et un layout mobile peuvent tous partager exactement le même cycle de vie des alertes tout en les affichant différemment. Le composant d’alerte peut également évoluer de manière indépendante sans toucher au hook qui gère le timing et la suppression.
Un piège similaire : les hooks qui retournent des composants
Un problème proche est celui où un hook définit et retourne une fonction de composant, comme const AlertView = () => ... à l’intérieur du corps du hook. Comme une nouvelle fonction est créée à chaque rendu, React perçoit un nouveau type de composant à chaque fois et démonte puis remonte cet arbre sous-jacent, effaçant son état interne et son DOM. Le fait de retourner des éléments, comme le fait le hook original, évite ce bug en particulier, mais le problème de conception décrit ci-dessus persiste.
Points clés
- L'utilisation de JSX à partir d’un hook signifie généralement que deux responsabilités ont été fusionnées en une seule abstraction.