Aller au contenu principal

Enchaîner les questions

Jouer une partie de 5 questions tirées au hasard, avec un bouton « Question suivante » et un écran de fin.

Objectifs de la séance​

  • mémoriser plusieurs informations avec plusieurs useState (les questions, l'index de la question courante) ;
  • afficher un écran différent selon l'état (question en cours ou partie terminée) ;
  • utiliser un initialiseur paresseux useState(() => ...) pour que le tirage au hasard ne soit fait qu'une fois ;
  • créer un bouton réutilisable avec une prop facultative ;
  • écrire la fonction pure preparerQuestions et ses tests.

Petite vérification​

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

  1. lancer l'application dans le navigateur, cliquer sur une mauvaise réponse et décrire les couleurs obtenues (bonne réponse, réponse choisie, autres réponses) ainsi que le message affiché sous les boutons
  2. cliquer ensuite sur une autre réponse et dire ce qui se passe
  3. exécuter npm test et lire le nombre de tests réussis
📌 Réponse attendue

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

Votre mission : transformer l'écran « une seule question » en une partie de 5 questions tirées au hasard (avec leurs réponses mélangées), qui se termine par un écran « Quiz terminé ! » et un bouton « Rejouer ».

Étape 1 — Préparer les questions d'une partie (fonction pure)​

Créez src/logic/preparerPartie.ts. La fonction preparerQuestions tire nombre questions au hasard dans la banque, puis mélange l'ordre des réponses de chacune (pour que la bonne réponse ne soit pas toujours au même endroit). Elle utilise les fonctions tirerAuHasard et melanger écrites au tutoriel 2.

Le début du fichier contient les importations (le type avec l'alias @/, la valeur avec un chemin relatif et l'extension .ts, comme la règle vue au tutoriel 2) :

import type { Question } from "@/types/quiz";

import { melanger, tirerAuHasard } from "./melanger.ts";

Bonne pratique - Renvoyer de nouveaux objets

{ ...question, reponses: ... } crée une copie de la question avec ses réponses mélangées. La banque QUESTIONS_LOCALES n'est jamais modifiée : sinon, mélanger les réponses d'une partie changerait aussi les parties suivantes.

Étape 2 — Tester la préparation​

Créez tests/preparerPartie.test.ts : le nombre de questions est correct, toutes sont différentes, les réponses sont conservées, et la banque d'origine n'est pas modifiée.

Lancez npm test : vous devez obtenir tests 15 et pass 15.


Bonne pratique - Tester aussi ce qui ne doit PAS arriver

Le troisième test vérifie que la banque n'a pas été modifiée. Les bugs les plus sournois sont des effets de bord que l'on n'a pas voulus : un test permet de les attraper avant l'utilisateur.

Étape 3 — Créer un bouton principal réutilisable​

Créez src/components/BoutonPrincipal.tsx : un grand bouton bleu pour les actions importantes (« Question suivante », « Rejouer »...). Sa prop desactive est facultative.

Le reste du composant est dans « Une solution » : il ressemble à BoutonReponse, avec desactive = false comme valeur par défaut dans les paramètres de la fonction.


Bonne pratique - Des valeurs par défaut pour les props facultatives

{ texte, onPress, desactive = false } : si l'appelant ne fournit pas desactive, le composant utilise false. Le code qui utilise le bouton reste court (<BoutonPrincipal texte="Rejouer" onPress={rejouer} />).

Étape 4 — Tirer les questions UNE seule fois​

Dans src/app/index.tsx, l'écran doit mémoriser la liste des questions de la partie. Au lieu de passer une valeur à useState, on lui passe une fonction : React ne l'appelle qu'au premier affichage.

N'oubliez pas les importations de BoutonPrincipal et de preparerQuestions, et la constante const NOMBRE_QUESTIONS = 5; au-dessus de la fonction Accueil.


Bonne pratique - Pas de hasard directement dans le rendu

Le composant est appelé à chaque affichage. Écrire const questions = preparerQuestions(...) directement dans la fonction changerait les questions à chaque clic. Le tirage doit se faire dans un initialiseur (une fois) ou dans un gestionnaire d'événement (au moment voulu).

Étape 5 — Mémoriser l'index de la question courante​

Il faut aussi savoir à quelle question on en est. Ajoutez un état indexQuestion (de 0 à 4) et calculez si la partie est terminée.


Bonne pratique - Les index commencent à 0

Avec 5 questions, les index valides sont 0, 1, 2, 3 et 4. La partie est terminée dès que l'index vaut 5 ou plus : on utilise donc >= plutôt que ===, plus prudent si l'index dépasse la valeur attendue.

Étape 6 — Passer à la question suivante​

Écrivez la fonction appelée par le bouton « Question suivante » : elle avance l'index et efface la réponse choisie, pour que la question suivante démarre « vierge ».

La fonction rejouer (dans « Une solution ») tire de nouvelles questions et remet l'index et la réponse à leur valeur de départ.


Bonne pratique - Remettre l'état à zéro ensemble

Quand on change de question, tout l'état lié à la question précédente (ici reponseChoisieId) doit être réinitialisé dans le même gestionnaire. Oublier une de ces remises à zéro donne des bugs étranges (« la nouvelle question est déjà corrigée »).

Étape 7 — Afficher l'écran de fin​

Quand la partie est terminée, la fonction Accueil renvoie un autre écran : « Quiz terminé ! » et un bouton « Rejouer ». Cette condition est placée avant le reste du JSX.


Bonne pratique - Sortir tôt avec un return

Traiter le cas particulier en premier (if (estTerminee) { return ...; }) évite d'écrire un gros if / else et surtout évite que la suite du code accède à questions[indexQuestion] quand cette question n'existe plus.

Étape 8 — Le bouton « Question suivante »​

Sous l'explication, ajoutez le BoutonPrincipal qui fait avancer la partie. Sur la dernière question, son texte devient « Voir le résultat ».

Testez une partie complète dans le navigateur : cinq questions, un écran de fin, puis « Rejouer ». Les questions et l'ordre des réponses doivent être différents à chaque partie.


Bonne pratique - Vérifier les cas limites

Jouez vraiment la dernière question (texte du bouton) et cliquez sur « Rejouer » : les erreurs se cachent presque toujours aux extrémités (première question, dernière question, fin de partie).

Étape 9 — Enregistrer votre travail​


Bonne pratique - Relire avant de valider

git status vous montre les fichiers modifiés avant le commit : vérifiez que vous retrouvez bien les fichiers de la séance (preparerPartie.ts, BoutonPrincipal.tsx, index.tsx, un fichier de test) et rien d'autre.

📌 Une solution

Notions théoriques​

Plusieurs états dans un même composant​

On peut appeler useState autant de fois que nécessaire. Chaque appel mémorise une information indépendante. L'écran de cette séance en possède trois :

ÉtatRôleValeur de départ
questionsles 5 questions de la partie, avec leurs réponses mélangéestirage au hasard
indexQuestionla position de la question courante dans la liste0
reponseChoisieIdla réponse du joueur à la question courantenull

Tout le reste (la question courante, la bonne réponse, estTerminee, estDerniere...) est calculé à partir de ces trois états.

Le rendu conditionnel : un écran selon l'état​

Une fonction composant peut renvoyer des JSX différents selon les valeurs de l'état :

if (estTerminee) {
return ( /* écran « Quiz terminé ! » */ );
}
// sinon, l'écran de la question en cours
const question = questions[indexQuestion];

Dans le JSX, on utilise aussi condition && ( ... ) (afficher ou non un bloc) et condition ? a : b (choisir entre deux valeurs). Ce sont des expressions : elles se placent entre accolades.

L'initialiseur paresseux​

// MAUVAIS : preparerQuestions est rappelée à CHAQUE affichage du composant
const [questions, setQuestions] = useState(preparerQuestions(QUESTIONS_LOCALES, 5));

// BON : la fonction n'est appelée qu'UNE fois, au premier affichage
const [questions, setQuestions] = useState(() => preparerQuestions(QUESTIONS_LOCALES, 5));

Dans le premier cas, la valeur passée à useState est calculée à chaque affichage (même si React ne garde que la première) : le tirage au hasard est refait pour rien, et le résultat est perdu. Dans le second cas, on donne à React une fonction : il l'appelle une seule fois, quand le composant apparaît. C'est l'initialiseur paresseux (lazy initializer).

Le piège du hasard dans le rendu

Pire : écrire const questions = preparerQuestions(...) sans useState redonne une liste différente à chaque affichage. Au premier clic sur une réponse, l'écran serait recalculé : la question changerait sous les yeux du joueur ! Règle à retenir : pas de hasard directement dans le corps d'un composant.

Une prop facultative​

Dans un type TypeScript, desactive?: boolean signifie « cette propriété peut être absente ». Dans ce cas, sa valeur est undefined, d'où la valeur par défaut desactive = false dans les paramètres du composant. Le même point d'interrogation existait déjà pour image?: string dans le type Question.

preparerQuestions : copier plutôt que modifier​

La fonction construit des nouveaux objets avec la syntaxe de décomposition (spread) :

{ ...question, reponses: melanger(question.reponses, aleatoire) }

Les trois points recopient toutes les propriétés de question, puis reponses est remplacé par la liste mélangée. Cette habitude (ne jamais modifier un objet ou un tableau existant, en créer un nouveau) est indispensable avec React : il détecte un changement en comparant les anciennes et les nouvelles valeurs.


Test de mémorisation/compréhension​


Que fait React de la fonction passée à useState(() => preparerQuestions(...)) ?


Pourquoi ne faut-il pas écrire const questions = preparerQuestions(...) directement dans le composant ?


Avec 5 questions, quand estTerminee (indexQuestion >= questions.length) devient-il vrai ?


Que signifie desactive?: boolean dans le type Props de BoutonPrincipal ?


Pourquoi preparerQuestions renvoie-t-elle de nouveaux objets (avec ...question) ?


Que doit faire la fonction passerALaSuite, en plus d'augmenter l'index ?



Exercices complémentaires​

Exercice A : une partie plus courte​

Changez la constante NOMBRE_QUESTIONS pour jouer des parties de 3 questions. Vérifiez que le texte « Question 1 / 3 » s'adapte tout seul : pourquoi n'avez-vous rien d'autre à modifier ?

Exercice B : un test pour une partie plus courte​

Dans tests/preparerPartie.test.ts, ajoutez un test qui vérifie que preparerQuestions(QUESTIONS_LOCALES, 3) renvoie bien 3 questions. Lancez npm test (vous devez obtenir 16 tests réussis).


Ce qu'il faut retenir​

NotionRésumé
Plusieurs étatsUn useState par information indépendante ; le reste est calculé.
Index courantindexQuestion désigne la question affichée ; la partie est finie quand il atteint questions.length.
Rendu conditionnelUn composant peut renvoyer des écrans différents (if + return), ou utiliser && et ? : dans le JSX.
Initialiseur paresseuxuseState(() => ...) : la fonction n'est appelée qu'au premier affichage.
HasardJamais directement dans le corps d'un composant : dans un initialiseur ou un gestionnaire.
Prop facultativenom?: type dans Props, et une valeur par défaut dans les paramètres.

Aperçu de la prochaine séance​

L'écran de fin affiche « Quiz terminé ! » mais pas encore le score. Dans la prochaine séance, vous allez le calculer. Vous verrez qu'il ne faut surtout pas le stocker dans un useState, mais le déduire des réponses données, et pourquoi une mise à jour écrite trop vite peut compter un point deux fois.