Aller au contenu principal

Mini-projet final : votre premier quiz

Tout ce que vous avez appris, dans une seule application : des questions, des réponses, un score et un écran de résultat.

Objectifs de la séance​

  • assembler composants, useState, listes et conditions dans une vraie application ;
  • gérer proprement la fin de partie (une valeur peut être undefined) ;
  • protéger l'application contre le double appui et les dépassements d'indice ;
  • réutiliser vos fonctions pures (calculerPourcentage, appreciation) ;
  • faire le bilan de vos compétences avant la série Expo.

Petite vérification​

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

  1. dans Expo Snack, avec le code de la séance précédente, appuyer sur Question 1 puis aussitôt sur Question 2 et dire quelle question s'affiche à la fin
  2. dire à quoi sert le tableau [id] à la fin de useEffect
  3. dire ce que fait controleur.abort() dans la fonction de nettoyage
📌 Réponse attendue

Notions théoriques​

Le cahier des charges​

Vous allez réaliser, dans un seul fichier App.tsx, un quiz de trois questions :

  1. l'écran affiche « Question 1 / 3 », l'énoncé et trois réponses ;
  2. un appui sur une réponse enregistre le choix (une seule fois) et compte un point si elle est correcte ;
  3. un bouton « Question suivante » apparaît une fois la réponse donnée ;
  4. après la dernière question, l'écran affiche le score, le pourcentage et l'appréciation, avec un bouton « Rejouer ».

Quels états ?​

Chaque donnée qui change doit être dans un état. Pas plus : ce qui se calcule ne se stocke pas.

ÉtatTypeRôle
indicenumbernuméro de la question en cours (de 0 à 2)
scorenumbernombre de bonnes réponses
choixnumber | nullid de la réponse choisie, ou null

Ne sont pas des états : la question courante (questions[indice]), le pourcentage et l'appréciation (calculés à chaque rendu).

Les protections à prévoir​

Posez-vous les questions de la partie 5 :

  • appui deux fois sur une réponse ? La garde if (choix !== null) return; ignore le second ;
  • appui sur « Question suivante » sans avoir répondu ? Le bouton n'existe pas tant que choix est null, et la fonction a sa propre garde ;
  • dépassement : après la dernière question, questions[indice] vaut undefined. On déclare const question: Question | undefined = ..., et TypeScript oblige à gérer ce cas avant d'utiliser question.enonce.

Test de mémorisation/compréhension​


Pourquoi le pourcentage n'est-il pas stocké dans un état ?


Quel est le type de la constante question dans const question: Question | undefined = questions[indice] ?


Quelle écriture passe à la question suivante sans valeur périmée ?


Que doit-on réécrire dans une liste de réponses affichée avec map ?


Quelle protection évite qu'un joueur gagne 5 points en appuyant 5 fois sur la bonne réponse ?



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

Votre mission : construire le quiz complet dans Expo Snack. Remplacez le contenu de App.tsx.

Point de départ (les données et les fonctions pures sont fournies) :

import { useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";

type Reponse = {
id: number;
texte: string;
estCorrecte: boolean;
};

type Question = {
id: number;
enonce: string;
reponses: Reponse[];
};

const questions: Question[] = [
{
id: 1,
enonce: "Quel mot de passe est le plus robuste ?",
reponses: [
{ id: 11, texte: "123456", estCorrecte: false },
{ id: 12, texte: "MonChienRex", estCorrecte: false },
{ id: 13, texte: "Velo-Rouge-Mange-7-Nuages!", estCorrecte: true },
],
},
{
id: 2,
enonce: "Qu'est-ce que l'hameçonnage ?",
reponses: [
{ id: 21, texte: "Une technique de pêche", estCorrecte: false },
{ id: 22, texte: "Un faux message pour voler vos informations", estCorrecte: true },
{ id: 23, texte: "Un type de pare-feu", estCorrecte: false },
],
},
{
id: 3,
enonce: "À quoi sert un VPN ?",
reponses: [
{ id: 31, texte: "À chiffrer votre connexion", estCorrecte: true },
{ id: 32, texte: "À accélérer le processeur", estCorrecte: false },
{ id: 33, texte: "À supprimer les virus", estCorrecte: false },
],
},
];

function calculerPourcentage(bonnesReponses: number, nombreQuestions: number): number {
if (nombreQuestions <= 0) {
return 0;
}
return Math.round((bonnesReponses / nombreQuestions) * 100);
}

function appreciation(pourcentage: number): string {
if (pourcentage >= 80) {
return "Excellent";
}
if (pourcentage >= 50) {
return "Bien";
}
return "À revoir";
}

Étape 1 — Les trois états​

Déclarez l'état indice (numéro de la question en cours, qui commence à 0).


Bonne pratique - Peu d'états, tout le reste se calcule

Trois états suffisent. Les ajouts « pratiques » (estTerminee, pourcentage) deviendraient des sources de contradictions s'ils étaient stockés.

Étape 2 — La question courante, qui peut ne plus exister​

Récupérez la question courante en déclarant explicitement qu'elle peut être undefined.


Bonne pratique - Annoncer l'absence possible

Sans cette annotation, TypeScript supposerait que questions[indice] existe toujours. En écrivant | undefined, vous l'obligez à vous rappeler de gérer la fin du quiz.

Étape 3 — Passer à la question suivante​

Écrivez la fonction suivante : elle ne fait rien si aucune réponse n'a été donnée, sinon elle avance et efface le choix.


Bonne pratique - Garde dans la fonction ET bouton masqué

Le bouton « Question suivante » n'est affiché que si choix !== null, mais la fonction vérifie quand même : un code robuste ne compte pas uniquement sur l'interface pour empêcher une action incorrecte.

Étape 4 — L'écran de résultat​

Quand question vaut undefined, le quiz est terminé : calculez le pourcentage et affichez le bilan.


Bonne pratique - Réutiliser les fonctions pures

calculerPourcentage et appreciation ont été écrites et testées dans la partie 2. Elles fonctionnent ici sans aucune modification : c'est tout l'intérêt de séparer calcul et affichage.

📌 Une solution

Pour aller plus loin​

Quand vous aurez terminé, relevez ces défis (ils sont facultatifs) :

  1. Feedback visuel : colorez en vert la bonne réponse et en rouge la réponse choisie quand elle est fausse (indice : style={[styles.bouton, condition && styles.bonne]}).
  2. Charger les questions : remplacez le tableau local par un useEffect qui charge les questions avec un « serveur » simulé (étudié à la séance précédente), avec un état chargement / succes / erreur.
  3. Chronomètre : affichez le temps écoulé avec setInterval et la fonction de nettoyage clearInterval.
  4. Mélange : mélangez l'ordre des questions au début de la partie, sans modifier le tableau d'origine (indice : [...questions].sort(...)).

Ce qu'il faut retenir​

NotionRésumé
Peu d'étatsStocker seulement ce qui change ; calculer le reste à chaque rendu.
Valeur possiblement absenteType | undefined oblige à gérer la fin de liste avant d'utiliser la valeur.
Gardesif (choix !== null) return; : protéger chaque action contre l'appui en double ou hors séquence.
Fonctions pures réutiliséesLes calculs écrits dans la partie 2 servent tels quels dans une interface.
Mises à jour fonctionnellessetX((ancien) => ...) dès que le nouvel état dépend de l'ancien.

Bilan du cours et suite​

Félicitations : vous avez terminé le cours ! Voici ce que vous savez désormais faire à la main :

  • écrire des fonctions claires, pures et typées ;
  • manipuler des objets et des tableaux sans jamais les modifier (map, filter, ...) ;
  • gérer les erreurs et ne jamais faire confiance à une donnée externe ;
  • écrire du code asynchrone robuste : async/await, annulation, protection contre le double appui et les réponses périmées ;
  • décrire vos données avec TypeScript ;
  • lire et écrire un composant React avec useState et useEffect.

Prochaine étape : la série Expo : un générateur de quiz Cyber. Vous y construirez, dans un vrai projet, une application complète avec navigation, base de données Supabase et classement.