Annuler une requête avec AbortController
Éviter les réponses qui arrivent dans le désordre en annulant les requêtes devenues inutiles, et compter les questions disponibles dans Supabase.
Objectifs de la séance
- comprendre le bug des réponses dans le désordre (et du
setStateaprès démontage) ; - annuler une requête avec un
AbortControlleret.abortSignal(signal); - vérifier
signal.abortedavant chaque mise à jour d'état et nettoyer dans leuseEffect; - compter des lignes sans les télécharger (
count: "exact",head: true) ; - mémoriser un résultat avec sa clé pour ignorer un résultat périmé ;
- ranger la logique dans deux hooks personnalisés :
useThemesetuseNombreQuestionsDisponibles.
Petite vérification
Avant de commencer, vérifiez si vous savez...
- dans le navigateur, couper le réseau (outils de développement
F12, onglet Réseau, Hors connexion), recharger le Générateur de quiz et lire le message affiché au-dessus du bouton - citer les trois valeurs possibles de la propriété
statutdu typeEtatChargement - citer le nom de la variable d'état de
generateur.tsxdont la modification relance le chargement des thèmes
📌 Réponse attendue
Notions théoriques
Le bug : des réponses qui arrivent dans le désordre
Dans le générateur, le joueur choisit un thème et un niveau. Pour chaque réglage, l'application doit afficher le nombre de questions disponibles, qu'elle demande à Supabase. Imaginez cette version « naïve » (elle est donnée ici pour comprendre le problème, ne l'écrivez pas) :
// Version NAÏVE : aucune protection contre les réponses tardives
useEffect(() => {
async function compter() {
const nombre = await compterQuestions(theme, niveau);
setNombreDisponible(nombre);
}
compter();
}, [theme, niveau]);
Le joueur est pressé : il clique sur « Réseaux et Wi-Fi » + « Expert », puis, une demi-seconde plus tard, sur « Tous » + « Tous ». Deux requêtes partent. Le réseau n'est pas régulier :
| Moment | Événement | Affichage |
|---|---|---|
| 0,0 s | clic sur Réseaux + Expert : la requête A part (lente) | « Comptage... » |
| 0,5 s | clic sur Tous + Tous : la requête B part (rapide) | « Comptage... » |
| 0,8 s | la réponse B arrive : 27 | 27 question(s) (correct) |
| 2,0 s | la réponse A arrive : 3 | 3 question(s) (faux : les réglages affichés sont Tous + Tous) |
L'écran montre un nombre qui ne correspond plus aux réglages choisis. Le joueur peut alors lancer une partie avec des réglages impossibles. C'est un bug intermittent : il dépend de la vitesse du réseau, donc il est difficile à reproduire et à corriger après coup.
Un second bug du même genre : quand l'écran disparaît pendant qu'une requête est en cours, la réponse arrive quand même et tente de mettre à jour l'état d'un composant qui n'existe plus.
Pour observer ces délais sur votre machine, ouvrez les outils du navigateur (F12), onglet Réseau,
et choisissez un débit lent (par exemple Slow 3G) : les requêtes durent alors plusieurs secondes.
L'AbortController : annuler une requête
Un AbortController est un objet JavaScript qui permet d'annuler une opération asynchrone.
Il fournit :
controleur.signal: un « signal » que l'on transmet à la requête ;controleur.abort(): la méthode qui déclenche l'annulation ;controleur.signal.aborted: un booléen qui vauttrueaprès l'annulation.
Avec Supabase, on transmet le signal à la requête grâce à .abortSignal(signal) :
const { data, error } = await supabase
.from("theme")
.select("code, libelle")
.abortSignal(signal);
Une requête annulée se termine en erreur (une erreur d'annulation). Il faut donc, dans le try comme
dans le catch, ne rien faire si le signal a été annulé : c'est le rôle du test if (!controleur.signal.aborted) placé avant chaque setState.
Nettoyer dans le useEffect
Une fonction renvoyée par un useEffect est sa fonction de nettoyage : React l'appelle avant de
relancer l'effet (quand une dépendance change) et quand le composant disparaît.
useEffect(() => {
const controleur = new AbortController();
// ... lancer la requête avec controleur.signal ...
return () => controleur.abort(); // nettoyage : annule la requête en cours
}, [theme, niveau]);
Résultat : quand le joueur change de réglage, la requête précédente est annulée avant que la nouvelle ne parte, et sa réponse tardive est ignorée.
Compter sans télécharger
Pour afficher « 27 question(s) disponible(s) », inutile de télécharger les 27 questions. Supabase sait renvoyer seulement le nombre de lignes :
supabase.from("question").select("id", { count: "exact", head: true });
count: "exact"demande le nombre exact de lignes qui correspondent ;head: truedemande de ne renvoyer aucune ligne (seulement l'en-tête de la réponse) ;- le nombre se lit dans la propriété
countdu résultat (qui peut êtrenull, d'oùcount ?? 0).
Les filtres .eq("theme_code", theme) et .eq("niveau", niveau) ne sont ajoutés que si le joueur a choisi
un thème ou un niveau précis (« tous » = pas de filtre).
Le résultat et sa clé
L'annulation ne suffit pas toujours : entre le moment où le joueur change de réglage et le moment où
l'effet est relancé, React affiche une fois l'ancien résultat avec les nouveaux réglages. Pour éviter
cet affichage périmé, le hook mémorise le résultat avec la clé des réglages qui l'ont produit
(${theme}/${niveau}, par exemple reseaux/3). À l'affichage :
- si la clé du résultat mémorisé correspond aux réglages actuels, on l'utilise ;
- sinon, on considère qu'on est encore en
chargement.
Cette comparaison ne demande aucun setState supplémentaire : l'état affiché est déduit des
réglages actuels et du dernier résultat.