Retour au cours

frontend / react

State avec useState

Leçon 31 exercice

Explication

Ce que vous allez apprendre

  • Différencier clairement une prop d'un state et savoir quand utiliser lequel
  • Déclarer un state avec useState et 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.

BesoinSyntaxeSûr si plusieurs appels enchaînés ?
Nouvelle valeur indépendantesetCompte(5)Oui
Nouvelle valeur dépendant de l'anciennesetCompte(c => c + 1)Oui, toujours sûr
Nouvelle valeur dépendant de l'anciennesetCompte(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.

jsx
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>
    );
}
jsx
// 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>;
}
jsx
// 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>
    );
}
jsx
// 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 état juste après setEtat() renvoie encore l'ancienne valeur.

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →