Charger les questions d'une partie
Remplacer les questions écrites dans le code par celles de Supabase, avec leurs quatre réponses, en vérifiant chaque question reçue.
Objectifs de la séance
- lire une question avec ses réponses en une seule requête (jointure
reponse(...)) ; - convertir une ligne de la base (
snake_case) enQuestionde l'application (camelCase), en écartant les lignes mal formées ; - écrire le hook
useQuestionsPartie(annulation, états, tirage au hasard après le chargement) ; - créer le composant
ChargementPartiequi affiche l'attente, l'erreur, puis la partie.
Petite vérification
Avant de commencer, vérifiez si vous savez...
- dans le Générateur de quiz, choisir le thème « Réseaux et Wi-Fi » et le niveau « Expert », puis lire le nombre de questions disponibles, puis faire de même avec Tous et Tous
- citer la méthode de l'objet
AbortControllerqui annule la requête en cours - citer l'option, passée à
select(...), qui demande à Supabase de ne renvoyer aucune ligne mais seulement leur nombre
📌 Réponse attendue
TP pour réfléchir et résoudre des problèmes
Le générateur compte maintenant les questions dans Supabase, mais la partie continue d'utiliser les
questions écrites dans questionsLocales.ts. Votre mission : charger les questions de la partie
depuis la base. Les explications détaillées suivent dans les Notions théoriques.
Étape 1 — Convertir une ligne de la base en Question
Supabase renvoie des lignes dont les colonnes sont en snake_case (theme_code, image_nom,
est_correcte). L'application utilise du camelCase (theme, image, estCorrecte). La
conversion se fait uniquement dans src/logic/conversions.ts, que vous complétez. Une question
mal formée (niveau inconnu, pas quatre réponses...) donne null : elle est écartée.
Remplacez la première ligne d'import de types par import type { CodeTheme, Niveau, Question, Theme } from "@/types/quiz";,
ajoutez import { estQuestionValide } from "./filtrerQuestions.ts"; (import de valeur : chemin relatif avec
extension), puis ajoutez à la fin du fichier :
Complétez aussi tests/conversions.test.ts (le fichier complet figure dans la solution de la séance),
puis lancez npm test.
Les noms snake_case de la base ne sortent jamais de conversions.ts. Le reste de l'application ne
connaît que des Question en camelCase : si une colonne change de nom, un seul fichier est à modifier.
Étape 2 — Écrire le service chargerQuestions
Dans src/services/questionsService.ts, ajoutez la fonction chargerQuestions. Sa particularité : le
select demande, en plus des colonnes de la question, ses réponses grâce à la jointure
reponse(id, texte, est_correcte).
Sans jointure, il faudrait une requête pour les questions, puis une par question pour ses réponses (le fameux problème « N+1 »). La jointure règle tout en un seul aller-retour réseau.
Étape 3 — Écrire le hook useQuestionsPartie
Créez src/hooks/useQuestionsPartie.ts. Il fonctionne comme useThemes (états, annulation, bouton
Réessayer), et il tire au hasard les nombre questions de la partie une seule fois, juste après le
chargement : jamais pendant le rendu, jamais dans le reducer.
Un composant React peut être « rendu » plusieurs fois. Un tirage au hasard placé dans le rendu changerait les questions à chaque affichage. Ici, il est fait une fois, dans le gestionnaire du chargement.
Étape 4 — Créer le composant ChargementPartie
Créez src/components/ChargementPartie.tsx. Ce composant appelle le hook, puis choisit quoi afficher :
l'attente, l'erreur avec Réessayer, ou la partie (PartieEnCours) une fois les questions prêtes.
PartieEnCours ne reçoit ses questions que lorsqu'elles existent. Elle n'a donc jamais à gérer
l'attente ni l'erreur : un composant, une responsabilité.
Étape 5 — Brancher la partie sur les questions chargées
Dans src/app/partie.tsx, affichez ChargementPartie à la place de PartieEnCours (importez-le, retirez l'import
de PartieEnCours). Dans src/components/PartieEnCours.tsx, la partie ne tire plus elle-même ses
questions : elle les reçoit dans une nouvelle propriété questions, qui initialise le reducer. Supprimez la
fonction genererQuestions et les imports devenus inutiles (QUESTIONS_LOCALES et preparerQuestions) ;
de @/logic/filtrerQuestions, ne gardez que trouverBonneReponse.
Testez dans le navigateur :
- générez un quiz de 5 questions : « Préparation du quiz... » apparaît brièvement, puis la partie, avec des questions venues de Supabase ;
- dans l'onglet Réseau (
F12), repérez la requêtequestion?select=...reponse(...): la réponse contient les questions et leurs réponses imbriquées ; - passez Hors connexion, rechargez la page de la partie : « Impossible de charger les questions. » avec Réessayer ; repassez en ligne, cliquez sur le bouton : la partie démarre.
Vous n'avez pas supprimé questionsLocales.ts : il sert encore à d'autres fichiers (et à générer le
seed). Supprimer du code parce qu'il n'est plus appelé se fait séparément, après vérification.
Étape 6 — Enregistrer votre travail
Cette séance modifie sept fichiers. Relire la liste de git status avant le commit permet de repérer un
fichier oublié ou, pire, un fichier qui ne devrait pas être là (comme .env.local).
📌 Une solution
Notions théoriques
Lire une question avec ses réponses : la jointure
Les réponses sont dans une autre table que les questions, reliée par la clé étrangère
reponse.question_id. Supabase sait suivre cette relation : dans le select, il suffit d'écrire le nom de
la table liée avec les colonnes voulues entre parenthèses.
.select("id, enonce, reponse(id, texte, est_correcte)")
Chaque ligne reçue contient alors un tableau reponse :
{
"id": 1,
"enonce": "Quel est le mot de passe le plus robuste ?",
"reponse": [
{ "id": 11, "texte": "123456", "est_correcte": false },
{ "id": 13, "texte": "Velo-Rouge-Mange-7-Nuages!", "est_correcte": true }
]
}
Le type LigneQuestion décrit cette forme. C'est la forme brute : elle ne devient une Question
qu'après la conversion.
Snake_case dans la base, camelCase dans l'application
La convention de la base de données est le snake_case (theme_code, est_correcte) ; celle de
JavaScript est le camelCase (theme, estCorrecte). Pour que les deux mondes ne se mélangent pas, la
conversion est faite à un seul endroit : la fonction convertirQuestion.
Une question mal formée ne doit pas planter l'application
convertirQuestion vérifie :
- le code de thème est connu (
estCodeTheme) et le niveau vaut 1, 2 ou 3 (estNiveau) ; - la question a bien quatre réponses dont une seule correcte (
estQuestionValide).
Sinon, elle renvoie null et la question est écartée du quiz. Mieux vaut jouer avec 26 questions
qu'avoir un écran qui plante parce qu'une question n'a que trois réponses. La colonne facultative
image_nom (qui peut valoir null) n'ajoute la propriété image que si elle contient une valeur.
La requête demande est_correcte : l'application connaît donc la bonne réponse de chaque question dès le
chargement, et un joueur averti peut la lire dans l'onglet Réseau. Vous corrigerez cette faille au
tutoriel 8, en laissant le serveur corriger les réponses.
Le hasard après le chargement
Le tirage au hasard des questions (preparerQuestions) a lieu une seule fois, dans le hook, quand les
questions viennent d'arriver. On ne le met jamais :
- dans le rendu du composant (il serait refait à chaque affichage) ;
- dans le reducer (une fonction pure doit toujours donner le même résultat pour les mêmes entrées).
Charger, puis jouer : le composant ChargementPartie
ChargementPartie est un composant « aiguilleur » : selon l'état du chargement, il affiche l'attente, l'erreur,
ou la partie. PartieEnCours n'est affiché qu'au moment où les questions existent, ce qui permet de
l'écrire comme si elles étaient toujours disponibles. C'est aussi la bonne façon de respecter la règle des
hooks : un hook ne peut pas être appelé dans une condition, donc on sépare le composant qui charge de celui
qui joue.
Test de mémorisation/compréhension
Exercices complémentaires
Exercice A : une nouvelle règle de validation
Ajoutez à tests/conversions.test.ts un test qui vérifie que convertirQuestion renvoie null pour une
question qui a deux bonnes réponses. Lancez npm test.
Exercice B : observer la requête
Dans F12 > Réseau, ouvrez la requête vers question et lisez le corps de la réponse (onglet Réponse).
Retrouvez, pour une question, ses quatre réponses imbriquées et la propriété est_correcte.
Ce qu'il faut retenir
| Notion | Résumé |
|---|---|
Jointure reponse(...) | Une seule requête renvoie les questions avec le tableau de leurs réponses. |
LigneQuestion | Forme brute reçue de la base, en snake_case. |
convertirQuestion | Seul endroit où la base devient l'application ; renvoie null si la question est mal formée. |
useQuestionsPartie | Charge avec annulation, gère les trois états et tire les questions au hasard une seule fois. |
ChargementPartie | Affiche l'attente, l'erreur ou la partie ; PartieEnCours reçoit des questions déjà prêtes. |
| Faille à venir | La bonne réponse est encore envoyée à l'application : elle sera retirée au tutoriel 8. |
Aperçu de la prochaine séance
Les questions viennent de la base, mais les illustrations sont encore des fichiers embarqués dans l'application. Dans la prochaine séance, vous allez les héberger dans Supabase Storage et les afficher à partir de leur adresse publique, en gérant le cas où une image ne se charge pas.