frontend / react
State avec useState
Explication
Ce que vous allez apprendre
- Différencier clairement une prop d'un state et savoir quand utiliser lequel
- Déclarer un state avec
useStateet déclencher un re-rendu en le modifiant - Utiliser la forme fonction
setValeur(v => ...)pour éviter le piège des mises à jour enchaînées - Mettre à jour un objet ou un tableau de state sans jamais le muter directement
- Initialiser un state coûteux à calculer avec une fonction paresseuse
Dans quel contexte ?
Sur src/components/CartCounter.jsx, un utilisateur clique trois fois rapidement sur "Ajouter au panier" mais le compteur n'affiche que +1 au lieu de +3. En inspectant le code, on trouve setCount(count + 1) répété trois fois dans la même fonction : les trois appels lisent la même valeur count figée du rendu en cours. C'est exactement le genre de bug que cette leçon permet de diagnostiquer et de corriger en quelques minutes une fois le mécanisme du state compris.
Étape 1 : une mémoire propre au composant
Le state (ou "état") est une donnée que garde en mémoire un composant, et qui peut changer au fil du temps : un compteur, le texte tapé dans un champ, une liste de tâches.
Étape 2 : la différence avec les props
Contrairement aux props, qui viennent de l'extérieur et sont figées pour le composant qui les reçoit, le state appartient au composant lui-même, qui peut le modifier.
Étape 3 : la particularité essentielle du state
Modifier le state ne se contente pas de changer une valeur en mémoire : cela déclenche automatiquement un nouveau rendu du composant pour que l'interface reflète ce changement.
Étape 4 : pourquoi useState existe
Avant les hooks, introduits en 2019, seule une syntaxe à base de classes permettait de gérer du state, ce qui était verbeux. useState permet d'ajouter du state directement à un composant fonction, de façon simple.
Étape 5 : comment useState fonctionne
useState(valeurInitiale) retourne une paire [valeur, setValeur]. Appeler setValeur(nouvelleValeur) ne modifie rien immédiatement sous vos yeux : React planifie un nouveau rendu, pendant lequel valeur prendra la nouvelle valeur.
Étape 6 : le premier piège, muter directement
Il reste deux erreurs très fréquentes à connaître. La première : muter directement un objet ou un tableau de state, au lieu de créer une nouvelle référence avec le spread { ...utilisateur, nom: "..." } — React compare les références, et une mutation directe passe inaperçue.
Piège fréquent
utilisateur.nom = "Marie"; setUtilisateur(utilisateur); ne provoque AUCUN re-rendu : React compare la référence de l'objet, qui n'a pas changé. Il faut toujours passer un nouvel objet : setUtilisateur({ ...utilisateur, nom: "Marie" }).
Le second piège, les mises à jour enchaînées
La seconde erreur consiste à enchaîner plusieurs mises à jour basées sur la valeur actuelle, qui utilisent toutes la même valeur figée du rendu en cours. La solution est la forme fonction setCompte(c => c + 1), qui reçoit toujours la dernière valeur réelle.
| Besoin | Syntaxe | Sûr si plusieurs appels enchaînés ? |
|---|---|---|
| Nouvelle valeur indépendante | setCompte(5) | Oui |
| Nouvelle valeur dépendant de l'ancienne | setCompte(c => c + 1) | Oui, toujours sûr |
| Nouvelle valeur dépendant de l'ancienne | setCompte(compte + 1) | Non, risque de valeur figée |
Et ensuite ?
Une fois le state compris, la prochaine étape consiste à réagir aux actions de l'utilisateur avec la gestion d'événements.
Commandes & code
State avec useState
Le state est une mémoire locale au composant qui, en changeant, déclenche un nouveau rendu.
import { useState } from "react";
function Compteur() {
const [compte, setCompte] = useState(0);
// useState(valeurInitiale) retourne [valeurActuelle, fonctionDeMiseAJour]
return (
<div>
<p>Compte : {compte}</p>
<button onClick={() => setCompte(compte + 1)}>+1</button>
<button onClick={() => setCompte(compte - 1)}>-1</button>
</div>
);
}// Le state est asynchrone/batché : ne JAMAIS lire l'ancienne valeur pour calculer la nouvelle
function CompteurBuggue() {
const [compte, setCompte] = useState(0);
function tripler() {
setCompte(compte + 1); // ces 3 appels utilisent tous la MÊME valeur "compte" figée
setCompte(compte + 1); // du rendu en cours -> résultat final : +1, pas +3
setCompte(compte + 1);
}
return <button onClick={tripler}>Compte : {compte}</button>;
}
// CORRECT : forme "updater", reçoit toujours la dernière valeur réelle
function CompteurCorrige() {
const [compte, setCompte] = useState(0);
function tripler() {
setCompte((c) => c + 1);
setCompte((c) => c + 1);
setCompte((c) => c + 1); // résultat final : +3, comme attendu
}
return <button onClick={tripler}>Compte : {compte}</button>;
}// State avec des objets/tableaux : toujours créer une NOUVELLE référence, jamais muter
function Formulaire() {
const [utilisateur, setUtilisateur] = useState({ nom: "", email: "" });
function majNom(e) {
// MAUVAIS : utilisateur.nom = e.target.value -- mutation directe, React ne redétecte pas le changement
setUtilisateur({ ...utilisateur, nom: e.target.value }); // BON : nouvel objet
}
return (
<input
value={utilisateur.nom}
onChange={majNom}
placeholder="Votre nom"
/>
);
}
function ListeTaches() {
const [taches, setTaches] = useState([]);
function ajouter(texte) {
setTaches([...taches, { id: Date.now(), texte, faite: false }]); // nouveau tableau
}
function basculer(id) {
setTaches(taches.map((t) =>
t.id === id ? { ...t, faite: !t.faite } : t
));
}
function supprimer(id) {
setTaches(taches.filter((t) => t.id !== id));
}
return (
<ul>
{taches.map((t) => (
<li key={t.id} onClick={() => basculer(t.id)}>
{t.faite ? "[x]" : "[ ]"} {t.texte}
</li>
))}
</ul>
);
}// Initialisation paresseuse : évite de recalculer une valeur coûteuse à chaque rendu
function Composant() {
const [donnees] = useState(() => calculCouteux()); // exécuté UNE SEULE FOIS, au montage
// useState(calculCouteux()) exécuterait la fonction À CHAQUE rendu, même si le résultat est ignoré
return <div>{donnees}</div>;
}Résumé
- Les mises à jour de state peuvent être batchées : utiliser la forme fonction
(prev) => ...si la nouvelle valeur dépend de l'ancienne. - Objets et tableaux de state doivent toujours être remplacés par une nouvelle référence, jamais mutés.
useState(() => ...)retarde le calcul de la valeur initiale à un seul appel, au montage.- Modifier le state déclenche un nouveau rendu ; lire
étatjuste aprèssetEtat()renvoie encore l'ancienne valeur.
Exercices pratiques
Mission : réparer le compteur de panier qui triche
Objectif : Diagnostiquer un bug de valeur figée sur des mises à jour de state enchaînées et corriger une mutation directe d'objet.
Contexte
Sur CartCounter.jsx, un clic sur "Ajouter 3 articles" doit incrémenter le compteur de 3, mais il n'avance que de 1. Le code fautif ressemble à setCount(count + 1); setCount(count + 1); setCount(count + 1);. Un second composant du même fichier gère un objet panier avec setPanier(panier) après avoir fait panier.total += prix, et l'affichage du total ne se met jamais à jour non plus.