Accueil / Articles

Concevoir des écrans React pour les états de chargement, vide, erreur et tentative de réessai

Apprenez à modéliser tous les états dans lesquels un écran et un formulaire React peuvent se trouver, des structures de base et des mises à jour jusqu’aux résultats vides, aux erreurs utiles et à la protection contre les soumissions doubles.

Éditeur : REACTAPP.TOP6 min de lecture1319 mots

Sources: Your React App Works. Until the API Doesn’t.

Même une interface qui s’affiche parfaitement avec une base de données locale prédéfinie peut échouer en production, où les utilisateurs font face à des réseaux lents, des tables vides et des points d’entrée défaillants. Ce guide décrit les états par lesquels passe réellement une interface React basée sur des données et ses formulaires, et vous propose une liste de contrôle pour concevoir chacun d’eux avant d’écrire du JSX.

Le modèle mental derrière la plupart des interfaces fragiles

Lorsque l’on esquisse un composant pour la première fois, le flux semble généralement suivre une ligne droite : demander quelque chose, puis l’afficher.

Fetch data → Show data

Ce schéma ne prévoit aucune marge d’erreur. Une interface en production prend des chemins différents : elle commence par se charger, aboutit à un résultat réussi ou vide, peut rencontrer une erreur, et nécessite un moyen de revenir en arrière par tentative répétée ou récupération des données.

Loading → Success → Empty
             ↓
           Error
             ↓
        Retry / Refetch

Considérer le chargement comme un état réel avec sa propre conception

« Afficher un indicateur de chargement » ressemble à une réponse complète, tout comme « l’encadrer dans un try/catch » : ce n’est pas faux, mais cela couvre rarement ce que vivent les utilisateurs. Deux questions révèlent cette lacune : à quoi ressemble l’écran lorsque la réponse met quatre secondes à arriver ? Et que se passe-t-il lorsqu’elle arrive en 40 millisecondes : l’utilisateur voit-t-il l’indicateur clignoter une seule fois avant de percevoir l’interface comme instable ?

Il est utile de diviser le chargement en deux situations distinctes :

  • Chargement initial. Rien n’apparaît encore à l’écran, donc un squelette reflétant le layout final constitue un choix raisonnable, voire agréable.
  • Rechargement. Les données sont déjà visibles, par exemple après que l’utilisateur ait appuyé sur « actualiser » ou modifié un filtre. Les effacer pour les remplacer par un indicateur de chargement complet fait perdre du contexte utile.

Lors d’un rechargement, conservez les données précédentes affichées et affichez un petit indicateur en ligne près de l’élément qui est mis à jour. Les indicateurs de chargement ne rassurent personne ; au mieux, ils sont tolérés. Pour des réponses très rapides, envisagez de retarder brièvement l’apparition de l’indicateur afin qu’il ne s’affiche que lorsque l’attente devient perceptible.

Pourquoi une requête fetch fonctionnelle n’est qu’une petite partie du travail

La requête elle-même est la partie simple :

const res = await fetch("/api/students");
const data = await res.json();
setStudents(data);

Ce fragment ne répond à rien de ce qui l’entoure. Que voit l’utilisateur pendant que la promesse est en attente ? Que se passe-t-il si fetch échoue, ou si le serveur répond avec un statut autre que 2xx (ce que fetch ne considère pas pour autant comme une erreur) ? Que se passe-t-il si le corps de la réponse est un tableau vide ? La question pertinente n’est pas de savoir comment obtenir les données, mais quels états cette requête peut avoir à tout moment.

Si students est le seul élément d’état, le reste de l’interface doit déduire la situation en fonction du fait que cette variable soit null, vide ou remplie. null peut signifier « en chargement » ou « la requête a échoué », et il n’y a aucun moyen de les distinguer. C’est du devinage déguisé en gestion d’état.

Un résultat vide est une réussite, pas une erreur

Ce point mérite d’être souligné car de nombreux projets le comprennent de manière erronée. Une réponse 200 OK contenant [] signifie que le serveur fait correctement son travail : il indique qu’il n’y a rien à afficher.

« Aucun étudiant trouvé » et « Impossible de charger les étudiants » décrivent des situations complètement différentes, pourtant de nombreuses applications affichent le même élément gris de remplacement pour les deux. L’utilisation d’un <p>Quelque chose a mal tourné</p> commun pour les deux cas mélange deux situations nécessitant des solutions distinctes : un état vide pourrait inviter l’utilisateur à supprimer le filtre, tandis qu’un état d’erreur devrait expliquer la panne et proposer des moyens de la résoudre.

À quoi ressemble un état d’erreur utile

L’affichage générique « Quelque chose a mal tourné » ne sert à personne. Un meilleur état d’erreur :

  • informe l’utilisateur de quelque chose qu’il peut faire, comme vérifier sa connexion ou réessayer plus tard ;
  • offre un bouton de réessai lorsque la tentative suivante a des chances réalistes de fonctionner ;
  • n’expose jamais d’empreinte d’exception brute ou de données d’erreur interne dans l’interface utilisateur ;
  • se limite à la partie de l’écran où la panne s’est produite.

Le dernier élément est celui qui est le plus souvent ignoré. Sur un tableau de bord comportant cinq widgets, une requête échouée ne devrait pas faire disparaître les quatre qui se sont chargées avec succès. Gestion des erreurs par section, via un état local ou des limites d’erreur autour de régions indépendantes.

Les formulaires suivent leur propre cycle de vie

L’écriture de données présente encore plus d’états que leur lecture :

Idle → Editing → Submitting → Success / Error

Prévention des soumissions dupliquées

Considérez un utilisateur qui clique sur « Soumettre » trois fois parce que le bouton ne fournit aucune indication visible après les deux premiers clics. À moins que le bouton ne soit désactivé pendant la soumission, ou que le gestionnaire ne vérifie un indicateur isSubmitting et ne renvoie immédiatement lorsque une demande est déjà en cours, le serveur reçoit trois demandes. Pour un formulaire de commande, cela signifie qu’un client est facturé trois fois pour un seul t-shirt. En ce qui concerne les paiements, le système backend doit également éliminer les doublons, par exemple à l’aide d’une clé d’idempotence.

Conserver les données saisies par l’utilisateur en cas d’échec de la soumission

Lorsque la demande échoue, laissez le formulaire tel quel. Forcer quelqu’un à réentrer un long message après un problème de réseau est une perte de temps. Les erreurs de validation, qu’elles proviennent de règles côté client ou du serveur, doivent apparaître à côté du champ concerné plutôt que dans une bannière vague en haut du formulaire.

Remplacer les booléens dispersés par des états nommés

Si on ne les contrôle pas, un composant a tendance à accumuler un ensemble de flags comme ceux-ci :

isLoading, isError, isSubmitting, hasData, isEmpty

Chaque flag est compréhensible individuellement. Ensemble, cinq booléens permettent 32 combinaisons, dont seules quelques-unes sont significatives, et rien n’empêche que isLoading && isError soit vrai dans les deux cas.

Une page listant les étudiants n’a pas besoin d’une bibliothèque formelle de machines à états. Ce dont elle a besoin, c’est d’une décision consciente, prise avant la première appel à useState, concernant les états distincts que l’écran peut occuper. Représentez l’état actuel par une seule valeur, comme un champ status contenant "loading", "success", "empty" ou "error", et laissez le rendu dépendre de cette valeur plutôt que de reconstituer la situation à partir de plusieurs indicateurs indépendants. En TypeScript, une union discriminée empêche la représentation des combinaisons impossibles ; l’article sur la typage de useState, useReducer et des hooks personnalisés aborde plus en détail l’aspect typage.

Une liste de contrôle à effectuer avant de créer l’écran suivant

Répondez à ces questions avant d’écrire le return de la composante :

  1. Qu’est-ce qui est visible avant l’arrivée de quelque donnée que ce soit ?
  2. Que se passe-t-il en cas d’échec de la requête ?
  3. Que se passe-t-il si elle réussit mais ne renvoie rien ?
  4. Que voit l’utilisateur pendant le rafraîchissement des données existantes ?
  5. Que se passe-t-il lors de la soumission d’un formulaire ?
  6. Que se passe-t-il si le formulaire est soumis deux fois ?
  7. Que se passe-t-il en cas d’échec de la validation ?

En résumé

La différence entre une démo et une interface en production réside rarement dans le scénario idéal. Elle se situe plutôt dans les états de chargement, de vide, d’erreur, de rafraîchissement et de soumission qui apparaissent dès que des utilisateurs réels et des réseaux réels interviennent. Nommez explicitement ces états, concevez chacun d’eux de manière intentionnelle, restreignez les échecs à leur niveau local, et protégez les entrées des utilisateurs ainsi que les effets sur le serveur contre des soumissions répétées ou échouées.

Lectures complémentaires