Aller au contenu principal

useState et événements

Répondre à une question : un clic, une mémoire, un écran qui se met à jour.

Objectifs de la séance​

  • comprendre pourquoi une variable ordinaire ne met pas l'écran à jour ;
  • utiliser useState (valeur, fonction de modification, valeur initiale) ;
  • réagir à un clic avec onClick ;
  • appliquer la règle d'or : remplacer l'état, ne jamais le modifier ;
  • utiliser la mise à jour fonctionnelle setX((ancien) => ...) ;
  • protéger l'interface contre le double clic.

Petite vérification​

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

  1. lancer npm run dev et compter les cartes affichées dans le navigateur
  2. dire pourquoi on écrit key={reponse.id} plutôt que key={index} dans un map
  3. dans data/questions.ts, remplacer le niveau 1 de la première question par 4 et lire l'erreur affichée par l'éditeur
📌 Réponse attendue

Notions théoriques​

Le problème : une variable ne suffit pas​

Essayons un compteur avec une variable ordinaire :

export function Compteur() {
let score = 0;

return (
<div>
<p>Score : {score}</p>
<button onClick={() => { score = score + 1; }}>+1</button>
</div>
);
}

La variable change bien, mais l'écran reste à 0 : React ne sait pas qu'il doit se redessiner. De plus, let score = 0 serait remise à 0 à chaque nouvel affichage.

La solution : useState​

useState donne au composant une mémoire qui survit aux affichages et qui prévient React quand elle change.

import { useState } from "react";

export function Compteur() {
const [score, setScore] = useState(0);

return (
<div>
<p>Score : {score}</p>
<button onClick={() => setScore(score + 1)}>+1</button>
</div>
);
}
  • useState(0) renvoie un tableau de deux éléments, que l'on récupère par destructuration (comme dans le cours JavaScript) ;
  • score est la valeur actuelle ; setScore est la fonction qui la remplace ;
  • 0 est la valeur initiale, utilisée seulement au premier affichage.

À chaque clic : setScore remplace la valeur, React rappelle la fonction du composant (on parle de rendu) avec la nouvelle valeur, puis met à jour la page.

Les événements​

En React, on attache un gestionnaire d'événement avec un attribut en camelCase : onClick, onChange, onSubmit... Il reçoit une fonction.

<button onClick={() => choisir(reponse)}> {/* JUSTE : on donne une fonction */}
<button onClick={choisir(reponse)}> {/* FAUX : appelle choisir tout de suite, au rendu */}

Chaque rendu a sa propre valeur​

Dans un rendu donné, score est une photo : il ne change pas, même après setScore.

function ajouterDeux() {
setScore(score + 1); // score vaut 0 dans ce rendu : on demande 0 + 1
setScore(score + 1); // toujours 0 : on demande à nouveau 0 + 1
}
// Résultat : le score passe de 0 à 1, et non à 2 !

Quand le nouvel état dépend de l'ancien, on passe une fonction à setScore : elle reçoit toujours la dernière valeur.

function ajouterDeux() {
setScore((ancien) => ancien + 1);
setScore((ancien) => ancien + 1);
}
// Résultat : le score passe de 0 à 2

Ne jamais modifier, toujours remplacer​

React détecte un changement en comparant l'ancienne et la nouvelle valeur. Un tableau ou un objet modifié « en place » reste le même objet : React ne voit rien.

const [themes, setThemes] = useState<string[]>([]);

themes.push("réseaux"); // FAUX : modifie l'original, l'écran ne bouge pas
setThemes((ancien) => [...ancien, "réseaux"]); // JUSTE : un nouveau tableau (spread)

Les règles des hooks​

useState est un hook (son nom commence par use). Deux règles :

  1. il s'appelle tout en haut du composant, jamais dans un if, une boucle ou une fonction imbriquée ;
  2. il s'appelle uniquement dans un composant (ou dans un autre hook).

Ne pas stocker ce qui se calcule​

Le pourcentage se déduit du score et du nombre de questions : on le calcule à chaque rendu au lieu de le stocker dans un état. Deux états qui doivent rester synchronisés finissent toujours par se contredire.

Se protéger du double clic​

Un joueur impatient peut cliquer deux fois très vite. Une garde en début de fonction ignore le second clic, et l'attribut disabled empêche d'insister :

function choisir(reponse: Reponse) {
if (choix !== null) {
return; // une réponse a déjà été donnée
}
setChoix(reponse.id);
}

Test de mémorisation/compréhension​


Que renvoie useState(0) ?


Pourquoi un compteur écrit avec let score = 0 ne met-il pas l'écran à jour ?


Que vaut score après setScore(score + 1); setScore(score + 1); depuis 0 ?


Quelle écriture ajoute "réseaux" à l'état themes (un tableau) correctement ?


Quelle écriture appelle choisir(reponse) lors d'un clic ?


Où doit-on appeler useState dans un composant ?



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

Votre mission : transformer la liste de questions en une vraie partie : une question à la fois, une correction immédiate, un score.

Étape 1 — Les calculs dans un fichier à part​

Créez src/logic/score.ts avec deux fonctions pures (celles de la partie 2 du cours JavaScript).


Bonne pratique - La logique hors des composants

Un calcul sans JSX se place dans src/logic/. Il reste simple à relire, et nous le testerons facilement dans le tutoriel 5.

Étape 2 — Les trois états​

Créez src/components/Partie.tsx. Déclarez l'indice de la question en cours, le score, et l'id de la réponse choisie (ou null).


Bonne pratique - Peu d'états

Trois états suffisent. La question courante, le pourcentage et l'appréciation se calculent : ne les stockez pas.

Étape 3 — Choisir une réponse, une seule fois​

Écrivez choisir : elle ignore le clic si une réponse est déjà choisie, enregistre le choix et ajoute un point si la réponse est correcte.


Bonne pratique - La garde protège le score

Sans le if (choix !== null) return;, un joueur pourrait cliquer dix fois sur la bonne réponse et gagner dix points. (Pour un vrai jeu, le calcul du score se fait côté serveur, car l'application du joueur peut être modifiée.)

Étape 4 — Passer à la question suivante​

Écrivez suivante et recommencer.


Bonne pratique - Garde dans la fonction et bouton masqué

Le bouton « Question suivante » n'apparaît que si une réponse est donnée, 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 5 — L'affichage avec la correction​

Affichez la question avec ses réponses sous forme de boutons : désactivés après le choix, la bonne réponse en vert, la mauvaise réponse choisie en rouge.


Bonne pratique - Un état non défini est géré

questions[indice] vaut undefined après la dernière question. La déclaration Question | undefined oblige TypeScript à vous faire gérer ce cas avant d'utiliser question.enonce.

📌 Une solution

Ce qu'il faut retenir​

NotionRésumé
useStateconst [valeur, setValeur] = useState(initiale) : mémoire qui prévient React d'un changement.
RenduChaque changement d'état rappelle la fonction du composant avec la nouvelle valeur.
ÉvénementonClick={() => f(x)} : on donne une fonction, on ne l'appelle pas.
Mise à jour fonctionnellesetX((ancien) => ...) dès que le nouvel état dépend de l'ancien.
Règle d'orNe jamais modifier l'état : le remplacer par une copie.
Gardeif (choix !== null) return; : protéger chaque action du double clic.

Aperçu de la prochaine séance​

Le joueur peut répondre, mais il ne peut pas encore se présenter. Dans la prochaine séance, vous allez créer un formulaire : saisie d'un pseudo, choix d'un niveau, validation des données. Vous découvrirez les champs contrôlés, la façon de React de gérer les input.