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...
- dans Expo Snack, avec le code de la séance précédente, compter les cartes affichées et dire laquelle affiche un indice
- 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 - dire pourquoi
{nombre && <Text>Bravo</Text>}peut faire planter l'écran quandnombrevaut0
📌 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
useStatede la partie 4 :[lire, ecrire]devient[score, setScore]; 0est la valeur initiale, utilisée seulement au tout premier affichage.
| Élément | Rôle |
|---|---|
score | la valeur actuelle de l'état (à lire) |
setScore | la 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
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 :
- on appelle un hook tout en haut du composant, jamais dans un
if, une boucle ou une fonction imbriquée ; - 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
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.
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.
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.
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.
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
| Notion | Résumé |
|---|---|
useState | const [valeur, setValeur] = useState(initiale) : une mémoire qui prévient React quand elle change. |
| Rendu | Chaque changement d'état rappelle la fonction du composant avec la nouvelle valeur. |
| Valeur périmée | Dans un rendu, l'état est une photo : utiliser setX((ancien) => ...) quand la valeur dépend de l'ancienne. |
| Règle d'or | Ne jamais modifier l'état : le remplacer par une copie ([...ancien, x], { ...ancien, a: 1 }). |
| Hooks | Appelés tout en haut d'un composant, jamais dans un if ou une boucle. |
onPress | Reç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.