Aller au contenu principal

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) en Question de 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 ChargementPartie qui affiche l'attente, l'erreur, puis la partie.

Petite vérification​

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

  1. 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
  2. citer la méthode de l'objet AbortController qui annule la requête en cours
  3. 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.


Bonne pratique - Convertir à un seul endroit

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).


Bonne pratique - Une requête plutôt que vingt-sept

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.


Bonne pratique - Le hasard ne se calcule pas pendant le rendu

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.


Bonne pratique - Séparer « charger » et « jouer »

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 :

  1. 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 ;
  2. dans l'onglet Réseau (F12), repérez la requête question?select=...reponse(...) : la réponse contient les questions et leurs réponses imbriquées ;
  3. 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.

Bonne pratique - Garder l'ancien code tant que le nouveau n'est pas validé

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​


Bonne pratique - Relire le git status

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.

Pour l'instant, la bonne réponse voyage jusqu'à l'application

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​


Que fait « reponse(id, texte, est_correcte) » dans un select de la table question ?


Pourquoi convertir les lignes de la base dans un fichier unique, conversions.ts ?


Que renvoie convertirQuestion pour une question qui n'a que trois réponses ?


Où le tirage au hasard des questions de la partie est-il fait ?


Pourquoi PartieEnCours n'est-elle affichée qu'une fois les questions chargées ?


Quelle faille subsiste à ce stade du projet, d'après la mise en garde de la séance ?



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​

NotionRésumé
Jointure reponse(...)Une seule requête renvoie les questions avec le tableau de leurs réponses.
LigneQuestionForme brute reçue de la base, en snake_case.
convertirQuestionSeul endroit où la base devient l'application ; renvoie null si la question est mal formée.
useQuestionsPartieCharge avec annulation, gère les trois états et tire les questions au hasard une seule fois.
ChargementPartieAffiche l'attente, l'erreur ou la partie ; PartieEnCours reçoit des questions déjà prêtes.
Faille à venirLa 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.