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
preparerQuestionset ses tests.
Petite vérification
Avant de commencer, vérifiez si vous savez...
- 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
- cliquer ensuite sur une autre réponse et dire ce qui se passe
- exécuter
npm testet 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";
{ ...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.
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.
{ 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.
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.
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.
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.
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.
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
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 :
| État | Rôle | Valeur de départ |
|---|---|---|
questions | les 5 questions de la partie, avec leurs réponses mélangées | tirage au hasard |
indexQuestion | la position de la question courante dans la liste | 0 |
reponseChoisieId | la réponse du joueur à la question courante | null |
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).
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
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
| Notion | Résumé |
|---|---|
| Plusieurs états | Un useState par information indépendante ; le reste est calculé. |
| Index courant | indexQuestion désigne la question affichée ; la partie est finie quand il atteint questions.length. |
| Rendu conditionnel | Un composant peut renvoyer des écrans différents (if + return), ou utiliser && et ? : dans le JSX. |
| Initialiseur paresseux | useState(() => ...) : la fonction n'est appelée qu'au premier affichage. |
| Hasard | Jamais directement dans le corps d'un composant : dans un initialiseur ou un gestionnaire. |
| Prop facultative | nom?: 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.