Aller au contenu principal

Charger les thèmes : chargement, erreur, succès

Remplacer la liste de thèmes écrite dans le code par celle de la base Supabase, en gérant proprement l'attente et les pannes.

Objectifs de la séance​

  • écrire une fonction asynchrone avec async, await et try/catch ;
  • représenter les trois états d'un chargement (chargement, erreur, succes) avec un type TypeScript ;
  • afficher un indicateur d'attente et un message d'erreur avec un bouton Réessayer ;
  • ranger les appels à Supabase dans un service et vérifier les données reçues avant de les utiliser.

Petite vérification​

Avant de commencer, vérifiez si vous savez...

  1. lancer l'application dans le navigateur (npx expo start, touche w) et lire le message affiché par l'accueil sous la présentation
  2. afficher dans un terminal le nom de la branche Git courante, puis les trois derniers commits, et lire le message du plus récent
  3. citer le préfixe obligatoire du nom d'une variable d'environnement lisible dans le code d'une application Expo
📌 Réponse attendue

TP pour réfléchir et résoudre des problèmes​

Jusqu'ici, l'écran « Générateur de quiz » affichait les thèmes écrits dans le code. Votre mission : les lire dans Supabase, en prévoyant les trois situations possibles : la réponse n'est pas encore arrivée, la requête a échoué, la requête a réussi. Les explications détaillées suivent dans les Notions théoriques.

Étape 1 — Créer la branche du tutoriel​

Repartez de main à jour (la Pull Request du tutoriel 6 a été fusionnée) et créez la branche du tutoriel 7 :


Bonne pratique - Une branche par fonctionnalité

Si le tutoriel se passe mal, main reste intact : il suffit d'abandonner la branche. C'est ce qui rend les essais sans risque.

Étape 2 — Décrire les trois états d'un chargement​

Créez le fichier src/types/chargement.ts. Il définit un type générique EtatChargement<T> : T est le type des données reçues en cas de succès (par exemple Theme[]). Les trois cas sont distingués par la propriété statut.


Bonne pratique - Rendre les états impossibles impossibles

Avec ce type, un chargement ne peut pas être « en erreur » et « réussi » en même temps, et les donnees n'existent qu'en cas de succès. TypeScript vous oblige à traiter chaque cas séparément.

Étape 3 — Créer le composant Chargement​

Créez src/components/Chargement.tsx : une roue qui tourne (ActivityIndicator) suivie d'un message. Ce composant sera réutilisé par tous les écrans qui attendent des données.


Bonne pratique - Dire à l'utilisateur qu'on travaille

Sans retour visuel, l'utilisateur croit que l'application est plantée et clique à nouveau. Un indicateur accompagné d'un message (« Chargement des thèmes... ») le rassure.

Étape 4 — Vérifier les données reçues​

Les données qui arrivent du réseau ne sont jamais garanties. Dans la base, rien n'empêche un administrateur d'ajouter un thème dont le code n'existe pas dans l'application. Créez src/logic/conversions.ts : la fonction convertirTheme transforme une ligne reçue en Theme, ou renvoie null si la ligne n'est pas utilisable.

Rappel : dans src/logic/, les imports de valeurs se font en chemin relatif avec l'extension (../constants/generateur.ts), pour que les tests node --test fonctionnent.

Ajoutez aussi le fichier de test tests/conversions.test.ts, puis lancez npm test : les deux tests doivent passer.

/// <reference types="node" />
import assert from "node:assert/strict";
import { test } from "node:test";

import { convertirTheme } from "../src/logic/conversions.ts";

test("convertirTheme accepte un thème connu", () => {
assert.deepEqual(convertirTheme({ code: "reseaux", libelle: "Réseaux et Wi-Fi" }), {
code: "reseaux",
libelle: "Réseaux et Wi-Fi",
});
});

test("convertirTheme écarte un code inconnu", () => {
assert.equal(convertirTheme({ code: "inconnu", libelle: "?" }), null);
});

Bonne pratique - Ne jamais faire confiance aux données reçues

Une donnée venue d'un réseau, d'un formulaire ou d'une URL peut être incorrecte. La valider à l'entrée de l'application évite qu'un code inconnu ne plante un écran bien plus loin.

Étape 5 — Écrire le service chargerThemes​

Une couche service regroupe les fonctions qui dialoguent avec Supabase : les écrans n'appellent jamais supabase directement. Créez src/services/themesService.ts. Le détail technique de l'erreur va dans la console (pour le développeur) ; un message simple est lancé vers l'écran (pour l'utilisateur).


Bonne pratique - Un message pour l'humain, un détail pour le développeur

Afficher « new row violates row-level security policy » à un joueur n'a aucun sens, et peut révéler des détails sur votre base. Gardez le détail dans la console et montrez un message compréhensible.

Étape 6 — Charger les thèmes dans l'écran générateur​

Dans src/app/generateur.tsx, les thèmes sont maintenant un état à trois cas, rempli par un useEffect. Le compteur tentative permet de relancer le chargement : changer sa valeur déclenche de nouveau l'effet.


Bonne pratique - Gérer aussi le cas d'échec

Un catch oublié, c'est une roue qui tourne pour toujours quand le réseau tombe. Un chargement doit toujours se terminer sur un des deux états finaux : succes ou erreur.

Étape 7 — Afficher les trois états​

Dans le JSX du générateur, remplacez la liste de thèmes par un affichage selon le statut. Le bouton Réessayer appelle la fonction reessayer.

Le test de connexion de l'accueil (étape 5 du tutoriel précédent) n'a plus d'utilité : retirez de src/app/index.tsx l'import de supabase, le useEffect, l'état messageConnexion, le <Text> qui l'affiche et le style info. L'accueil retrouve son code d'avant.

Testez ensuite, dans le navigateur :

  1. ouvrez le Générateur : la roue d'attente apparaît brièvement, puis les trois thèmes ;
  2. ouvrez les outils du navigateur (F12), onglet Réseau, choisissez Hors connexion, puis rechargez la page du générateur : le message « Impossible de charger les thèmes. » s'affiche avec le bouton Réessayer ;
  3. repassez En ligne et cliquez sur Réessayer : les thèmes apparaissent.

Bonne pratique - Tester la panne, pas seulement le cas heureux

Un écran qui fonctionne quand tout va bien n'est qu'à moitié terminé. Couper le réseau est le test le plus simple (et le plus révélateur) du comportement d'une application.

Étape 8 — Enregistrer votre travail​


Bonne pratique - Pousser la branche dès le premier commit

-u mémorise le lien entre votre branche locale et GitHub : les prochains envois de ce tutoriel se feront avec un simple git push.

📌 Une solution

Notions théoriques​

Le code asynchrone : async, await, try/catch​

Une requête vers Supabase prend du temps (de quelques dizaines de millisecondes à plusieurs secondes, ou jamais si le réseau est coupé). Le code ne peut pas « attendre » en bloquant l'écran : il est asynchrone.

  • une fonction async peut utiliser le mot-clé await, qui suspend la fonction jusqu'à l'arrivée de la réponse, sans bloquer le reste de l'application ;
  • une fonction async renvoie une promesse (Promise) : une valeur qui sera disponible plus tard ;
  • try { ... } catch (erreur) { ... } exécute le code du try et, si une erreur est lancée (throw), passe au catch.
async function charger() {
try {
const themes = await chargerThemes(); // on attend la réponse
// ... utiliser themes
} catch (erreur) {
// ... la requête a échoué
}
}

Les trois états d'un chargement​

Pendant une requête, l'écran est toujours dans un et un seul de ces états :

StatutSignificationCe que voit l'utilisateur
chargementla réponse n'est pas encore arrivéeune roue et un message
erreurla requête a échouéun message clair et un bouton Réessayer
succesles données sont arrivéesles thèmes

Un type union discriminée (EtatChargement<T>) exprime cela en TypeScript : la propriété statut est la « discriminante ». Quand on teste etat.statut === "succes", TypeScript sait que etat.donnees existe ; dans les autres cas, il refuse d'y accéder.

L'écran blanc et la roue éternelle

Les deux bugs les plus fréquents du code asynchrone : ne rien afficher pendant l'attente (écran blanc) et oublier le cas d'erreur (la roue tourne pour toujours). Toute donnée venue du réseau doit prévoir ses trois états.

Le bouton Réessayer et la variable tentative​

Un useEffect s'exécute de nouveau quand l'une de ses dépendances change. En ajoutant tentative aux dépendances ([tentative]), on obtient un moyen simple de relancer le chargement : le bouton Réessayer repasse l'état à chargement puis incrémente tentative.

Service, écran, logique : chacun son rôle​

Le projet est organisé en couches :

CoucheDossierRôle
écransrc/app/affiche, ne connaît pas Supabase
servicesrc/services/seul endroit qui appelle Supabase et fabrique les messages d'erreur utilisateur
logiquesrc/logic/fonctions pures, testées par npm test

Cette séparation a un intérêt concret : si l'on change de base de données un jour, seuls les services changent, et la logique de conversion reste testable sans réseau.

La conversion vérifiée​

TypeScript ne vérifie les types qu'à la compilation. À l'exécution, rien ne garantit que les lignes reçues de Supabase ont la forme annoncée. La fonction convertirTheme accepte une ligne { code: string; libelle: string } et vérifie que le code fait bien partie des codes connus de l'application : estCodeTheme(texte: string): texte is CodeTheme est un type guard (« garde de type ») : après un test réussi, TypeScript considère que texte est un CodeTheme.

Les lignes invalides sont écartées (null) plutôt que de faire planter l'application.

Une liste de thèmes volontairement fermée

Le type CodeTheme ne connaît que trois codes. Ajouter un thème dans la base obligerait aussi à modifier l'application : c'est un choix de simplicité assumé pour cette série.

Le drapeau estAnnule : une première protection​

Le useEffect utilise encore un drapeau estAnnule, déjà vu à la séance précédente : si l'écran disparaît pendant la requête, on évite de mettre à jour l'état. Mais la requête continue de s'exécuter pour rien. Dans la prochaine séance, vous la ferez réellement annuler.


Test de mémorisation/compréhension​


Que fait le mot-clé await dans une fonction async ?


Combien d'états possibles a un chargement dans l'application ?


Que se passe-t-il si l'on oublie le catch autour de la requête et que le réseau est coupé ?


À quoi sert la variable tentative dans le générateur ?


Pourquoi les écrans n'appellent-ils jamais supabase directement ?


Pourquoi convertirTheme renvoie-t-elle null pour un code de thème inconnu ?



Exercices complémentaires​

Exercice A : un message plus précis​

Dans themesService.ts, remplacez temporairement le nom de la table theme par themes (qui n'existe pas). Observez le message affiché dans l'application et le détail technique dans la console du navigateur. Remettez ensuite le bon nom.

Exercice B : un test supplémentaire​

Ajoutez à tests/conversions.test.ts un test qui vérifie que convertirTheme accepte le thème { code: "hameconnage", libelle: "Hameçonnage" }. Lancez npm test.


Ce qu'il faut retenir​

NotionRésumé
async / awaitÉcrire du code qui attend une réponse sans bloquer l'application.
try / catchTraiter l'échec d'une requête : sans catch, la roue tourne pour toujours.
EtatChargement<T>Union de trois états explicites : chargement, erreur, succes.
tentativeCompteur ajouté aux dépendances d'un useEffect pour relancer un chargement.
ServiceSeul endroit qui appelle Supabase ; renvoie des messages d'erreur simples.
Conversion vérifiéeLes données du réseau ne sont jamais garanties : on les valide et on écarte les lignes invalides.
Tester la panneCouper le réseau montre si l'application gère vraiment l'erreur.

Aperçu de la prochaine séance​

Votre générateur gère l'attente et l'échec, mais un problème discret subsiste : si le joueur change très vite de thème, plusieurs requêtes partent en même temps et leurs réponses peuvent arriver dans le désordre. Dans la prochaine séance, vous provoquerez ce bug, puis vous le corrigerez avec l'AbortController, et vous afficherez le nombre de questions disponibles pour chaque réglage.