Aller au contenu principal

L'état avec useState

Donner une mémoire à votre écran : quand une donnée change, l'affichage suit tout seul.

Objectifs de la séance​

  • comprendre pourquoi une variable ordinaire ne met pas l'écran à jour ;
  • utiliser useState : valeur, fonction de modification, valeur initiale ;
  • appliquer la règle d'or : ne jamais modifier l'état, toujours le remplacer ;
  • utiliser la mise à jour fonctionnelle setScore((ancien) => ancien + 1) ;
  • protéger un bouton contre le double appui ;
  • éviter de stocker ce qui peut se calculer.

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, compter les cartes affichées et dire laquelle affiche un indice
  2. dire ce qu'il faut écrire à la place de key={question.id} pour que React identifie correctement chaque carte, et pourquoi on n'utilise pas l'indice du tableau
  3. dire pourquoi {nombre && <Text>Bravo</Text>} peut faire planter l'écran quand nombre vaut 0
📌 Réponse attendue

Notions théoriques​

Le problème : une variable ne suffit pas​

Essayons un compteur avec une variable ordinaire :

export default function App() {
let score = 0;

function ajouterUnPoint() {
score = score + 1; // la variable change...
console.log(score);
}

return (
<View>
<Text>Score : {score}</Text>
<Pressable onPress={ajouterUnPoint}><Text>+1</Text></Pressable>
</View>
);
}

La console affiche bien 1, 2, 3... mais l'écran reste à 0 : React ne sait pas que la variable a changé, donc il ne ré-affiche pas le composant. (Et même s'il le faisait, let score = 0 serait remise à 0 à chaque 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";

const [score, setScore] = useState(0);

Reconnaissez ce que vous avez déjà étudié :

  • c'est la destructuration de tableau (partie 3) : useState(0) renvoie un tableau de deux éléments ;
  • c'est le mini useState de la partie 4 : [lire, ecrire] devient [score, setScore] ;
  • 0 est la valeur initiale, utilisée seulement au tout premier affichage.
ÉlémentRôle
scorela valeur actuelle de l'état (à lire)
setScorela fonction qui remplace la valeur et demande un nouvel affichage
useState(0)la valeur initiale
export default function App() {
const [score, setScore] = useState(0);

return (
<View>
<Text>Score : {score}</Text>
<Pressable onPress={() => setScore(score + 1)}>
<Text>+1</Text>
</Pressable>
</View>
);
}

À chaque appui : setScore remplace la valeur, React rappelle la fonction App (c'est un rendu) avec la nouvelle valeur, et l'écran est mis à jour.

Chaque rendu a sa propre valeur (retour des fermetures)​

Dans un rendu donné, score est une photo : elle ne change pas pendant ce rendu, même après setScore. Cela rappelle la « valeur périmée » de la partie 4.

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 !

La mise à jour fonctionnelle​

Quand le nouvel état dépend de l'ancien, passez à setScore une fonction : 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
Bonne pratique - Règle de la mise à jour fonctionnelle

Si la nouvelle valeur dépend de l'ancienne (+1, ajout à une liste, inversion d'un booléen), écrivez toujours setX((ancien) => ...). C'est la même idée que « relire la donnée après l'attente » de la partie 5.

Règle d'or : ne jamais modifier, toujours remplacer​

React détecte un changement en comparant l'ancienne et la nouvelle valeur. Si vous modifiez un tableau ou un objet en place, l'ancien et le nouveau sont le même objet : React ne voit rien.

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

// FAUX : modifie le tableau d'origine, l'écran ne bouge pas
themes.push("réseaux");
setThemes(themes);

// JUSTE : un nouveau tableau (spread, partie 3)
setThemes((ancien) => [...ancien, "réseaux"]);

// JUSTE : pour un objet
setJoueur((ancien) => ({ ...ancien, score: ancien.score + 1 }));

Reconnaissez le spread et la fonction pure : c'est exactement pour cela que vous les avez travaillés.

Typer un état​

TypeScript déduit le type de la valeur initiale. Quand elle ne suffit pas (valeur absente, tableau vide), précisez le type entre chevrons :

const [score, setScore] = useState(0); // number (déduit)
const [choix, setChoix] = useState<number | null>(null); // id de la réponse choisie, ou rien
const [historique, setHistorique] = useState<string[]>([]); // tableau de textes

Les règles des hooks​

useState est un hook (les noms commencent par use). Deux règles :

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

Réagir à un appui : onPress​

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

onPress attend une fonction (un callback, partie 2), pas le résultat d'un appel.

Se protéger du double appui​

Un joueur peut appuyer deux fois très vite sur la même réponse. Une garde en début de fonction empêche le second traitement :

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

Ne pas stocker ce qui se calcule​

Si une valeur se déduit d'autres états, ne la stockez pas : calculez-la à chaque rendu. Deux états qui doivent rester synchronisés finissent toujours par se contredire.

const [score, setScore] = useState(0);
const [total, setTotal] = useState(0);

// FAUX : un troisième état à maintenir à la main
// const [pourcentage, setPourcentage] = useState(0);

// JUSTE : une simple constante recalculée à chaque rendu
const pourcentage = total === 0 ? 0 : Math.round((score / total) * 100);

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 le score après : setScore(score + 1); setScore(score + 1); avec un score initial de 0 ?


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


Où doit-on appeler useState dans un composant ?


Quelle écriture est correcte pour appeler choisir(reponse) lors d'un appui ?



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

Votre mission : transformer une question à choix multiples en un écran interactif avec un score. Dans Expo Snack, remplacez le contenu de App.tsx.

Point de départ :

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

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

const reponses: Reponse[] = [
{ id: 11, texte: "123456", estCorrecte: false },
{ id: 12, texte: "MonChienRex", estCorrecte: false },
{ id: 13, texte: "Velo-Rouge-Mange-7-Nuages!", estCorrecte: true },
];

Étape 1 — Le score en mémoire​

Déclarez l'état du score, avec la valeur initiale 0.


Bonne pratique - setX pour la fonction de modification

Nommez la fonction de modification set suivi du nom de l'état (score et setScore) : tout développeur React comprend immédiatement le lien.

Étape 2 — Retenir la réponse choisie​

Ajoutez un état choix qui contient l'id de la réponse choisie, ou null tant qu'aucune n'est choisie.


Bonne pratique - null pour « pas encore de valeur »

null marque explicitement « rien de choisi ». Le type number | null oblige TypeScript à vous rappeler de gérer les deux cas partout où vous utilisez choix.

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

Écrivez la fonction choisir : elle ignore l'appui si une réponse est déjà choisie, enregistre le choix, et ajoute 1 au score si la réponse est correcte.


Bonne pratique - La garde protège le score

Sans le if (choix !== null) return;, un joueur pourrait appuyer 10 fois sur la bonne réponse et gagner 10 points. Ce genre de triche se prévient toujours au plus près de l'action (et, pour les vrais jeux, côté serveur).

Étape 4 — Garder l'historique sans le modifier​

Ajoutez un état historique (tableau de textes) et enregistrez chaque réponse donnée, sans modifier le tableau d'origine.


Bonne pratique - Jamais de push sur un état

historique.push(...) modifierait le tableau d'origine : l'écran ne se mettrait pas à jour. [...ancien, nouvelElement] crée un nouveau tableau que React peut comparer.

📌 Une solution

Ce qu'il faut retenir​

NotionRésumé
useStateconst [valeur, setValeur] = useState(initiale) : une mémoire qui prévient React quand elle change.
RenduChaque changement d'état rappelle la fonction du composant avec la nouvelle valeur.
Valeur périméeDans un rendu, l'état est une photo : utiliser setX((ancien) => ...) quand la valeur dépend de l'ancienne.
Règle d'orNe jamais modifier l'état : le remplacer par une copie ([...ancien, x], { ...ancien, a: 1 }).
HooksAppelés tout en haut d'un composant, jamais dans un if ou une boucle.
onPressReçoit une fonction : onPress={() => choisir(reponse)}.
État dérivéNe pas stocker ce qui peut être calculé à partir d'autres états.

Aperçu de la prochaine séance​

Votre écran réagit aux appuis. Mais une application doit aussi aller chercher des données (sur un serveur), lancer une minuterie, nettoyer derrière elle. C'est le rôle de useEffect, où vous retrouverez tout ce que vous avez appris sur l'asynchrone et l'annulation des requêtes.