useState et useEffect : Le Guide Pratique des Hooks React

8 min de lecture React

Quand React a introduit les hooks en 2019, ça a changé la donne pour tout le monde. Fini les classes interminables avec this.state, this.setState() et les méthodes de cycle de vie éparpillées partout. Aujourd'hui, avec deux hooks — useState et useEffect — tu peux gérer l'état et les effets de bord dans des composants fonctionnels simples et lisibles. Si tu débutes avec React, ces deux hooks sont les premiers que tu vas apprendre et les plus utilisés au quotidien. Ce guide t'explique tout, de la syntaxe de base aux erreurs à éviter.

Avant les hooks : le monde des class components

Pour comprendre pourquoi useState et useEffect sont si importants, il faut mesurer ce qu'on faisait avant. En React "classique", chaque composant avec de l'état devait être une classe :

class Compteur extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  componentDidMount() {
    document.title = `Compteur : ${this.state.count}`;
  }

  componentDidUpdate() {
    document.title = `Compteur : ${this.state.count}`;
  }

  render() {
    return (
      <button onClick={() => this.setState({ count: this.state.count + 1 })}>
        Clics : {this.state.count}
      </button>
    );
  }
}

Deux méthodes pour la même logique, le mot-clé this partout, un constructeur obligatoire. Avec les hooks, tout ça s'écrit en une dizaine de lignes claires. C'est ça la révolution.

useState : la syntaxe de base

useState est un hook qui te permet d'ajouter de l'état local à un composant fonctionnel. La syntaxe suit toujours le même schéma :

const [état, setÉtat] = useState(valeur_initiale);

Tu récupères un tableau de deux éléments : la valeur actuelle de l'état, et une fonction pour la mettre à jour. Voici le même compteur réécrit avec les hooks :

import { useState, useEffect } from 'react';

function Compteur() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `Compteur : ${count}`;
  }, [count]);

  return (
    <button onClick={() => setCount(count + 1)}>
      Clics : {count}
    </button>
  );
}

Infiniment plus clair. Et toute la logique liée au titre de la page est au même endroit, pas dispersée dans deux méthodes séparées.

useState avec différents types de valeurs

La valeur initiale passée à useState peut être de n'importe quel type JavaScript.

String, number, boolean

const [nom, setNom] = useState('');
const [score, setScore] = useState(0);
const [isOpen, setIsOpen] = useState(false);

// Toggle booléen
<button onClick={() => setIsOpen(prev => !prev)}>
  {isOpen ? 'Fermer' : 'Ouvrir'}
</button>

Array et object

const [items, setItems] = useState([]);
const [user, setUser] = useState({ nom: '', email: '' });

Pour les tableaux et objets, fais attention à la mise à jour — la règle est de ne jamais muter directement.

Mettre à jour le state correctement : ne jamais muter

C'est la règle fondamentale de React : tu ne modifies jamais l'état directement. Tu crées toujours une nouvelle valeur. C'est le même principe que pour la manipulation du DOM en JavaScript — React détecte les changements en comparant les références, et si tu mutes directement, il ne voit rien.

// ❌ Mutation directe — React ne re-rend pas
const ajouterItem = () => {
  items.push('nouvel item');
  setItems(items); // même référence = pas de re-rendu
};

// ✅ Nouveau tableau
const ajouterItem = () => {
  setItems([...items, 'nouvel item']);
};

// ❌ Pour un objet
user.nom = 'Alice';
setUser(user);

// ✅ Spread operator
setUser({ ...user, nom: 'Alice' });

La forme fonctionnelle est aussi très utile quand la nouvelle valeur dépend de l'ancienne :

// Toujours préférer cette forme pour les incrémentations
setCount(prev => prev + 1);

Elle garantit que tu travailles avec la dernière valeur, même si React groupe plusieurs mises à jour.

useEffect : la syntaxe de base

useEffect sert à gérer les effets de bord : appels API, abonnements, timers, modifications du DOM... Tout ce qui n'est pas du rendu pur.

useEffect(() => {
  // ton effet ici
}, [dépendances]);

Le premier argument est une fonction qui contient l'effet. Le deuxième est le tableau de dépendances, qui contrôle quand l'effet se déclenche. Consulte la documentation officielle de useEffect pour explorer tous les cas avancés.

Le tableau de dépendances expliqué

C'est le point qui crée le plus de confusion. Il y a trois cas :

Tableau vide [] — une seule fois au montage

useEffect(() => {
  console.log('Le composant est monté');
  // Équivalent de componentDidMount
}, []);

Tableau avec valeurs — à chaque changement

useEffect(() => {
  console.log(`userId a changé : ${userId}`);
  // Se déclenche au montage + à chaque fois que userId change
}, [userId]);

Sans tableau — à chaque rendu

useEffect(() => {
  console.log('Re-rendu !');
  // Se déclenche à CHAQUE rendu — rarement utile, souvent dangereux
});

Nettoyer les effets avec le cleanup

Certains effets ont besoin d'être nettoyés quand le composant disparaît : timers, abonnements, event listeners. Pour ça, tu retournes une fonction depuis ton effet :

useEffect(() => {
  const timer = setInterval(() => {
    setSecondes(prev => prev + 1);
  }, 1000);

  // Cleanup : s'exécute quand le composant est démonté
  return () => {
    clearInterval(timer);
  };
}, []);

Autre exemple avec un event listener :

useEffect(() => {
  const handleResize = () => setLargeur(window.innerWidth);
  window.addEventListener('resize', handleResize);

  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

Sans cleanup, tu accumules des listeners à chaque montage du composant — fuites mémoire et comportements imprévisibles assurés.

Combiner useState + useEffect : fetch API

Le cas le plus classique : charger des données depuis une API au montage. C'est là que les deux hooks travaillent vraiment ensemble.

import { useState, useEffect } from 'react';

function ListeArticles() {
  const [articles, setArticles] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [erreur, setErreur] = useState(null);

  useEffect(() => {
    const fetchArticles = async () => {
      try {
        const response = await fetch('https://api.exemple.com/articles');
        if (!response.ok) throw new Error('Erreur réseau');
        const data = await response.json();
        setArticles(data);
      } catch (err) {
        setErreur(err.message);
      } finally {
        setIsLoading(false);
      }
    };

    fetchArticles();
  }, []);

  if (isLoading) return <p>Chargement...</p>;
  if (erreur) return <p>Erreur : {erreur}</p>;

  return (
    <ul>
      {articles.map(article => (
        <li key={article.id}>{article.titre}</li>
      ))}
    </ul>
  );
}

Trois états distincts pour gérer proprement les trois situations : chargement, erreur, succès. C'est le pattern standard que tu vas retrouver dans quasiment tous les projets React réels.

Les 3 erreurs classiques avec ces hooks

Pour une liste complète des pièges à éviter, consulte notre guide sur les erreurs courantes avec les hooks React. Voici les trois plus fréquentes :

1. Dépendances manquantes dans useEffect

// ❌ userId utilisé mais absent du tableau de dépendances
useEffect(() => {
  fetch(`/api/user/${userId}`).then(/* ... */);
}, []); // userId manque !

// ✅ Correct
useEffect(() => {
  fetch(`/api/user/${userId}`).then(/* ... */);
}, [userId]);

Le plugin ESLint eslint-plugin-react-hooks t'avertit automatiquement. Écoute ces avertissements — ils préviennent des bugs où ton effet utilise une valeur "périmée".

2. Mutation directe du state

// ❌ splice mute le tableau original
const supprimerItem = (id) => {
  const index = items.findIndex(i => i.id === id);
  items.splice(index, 1);
  setItems(items); // même référence = pas de re-rendu
};

// ✅ filter crée un nouveau tableau
const supprimerItem = (id) => {
  setItems(items.filter(i => i.id !== id));
};

3. setState dans useEffect sans condition — la boucle infinie

// ❌ Boucle infinie !
// count change → effet → setCount → count change → ...
useEffect(() => {
  setCount(count + 1);
}, [count]);

// ✅ Si nécessaire, forme fonctionnelle + tableau vide
useEffect(() => {
  setCount(prev => prev + 1);
}, []); // Une seule fois

La boucle infinie est la plus spectaculaire des erreurs React — elle fait exploser la mémoire ou geler le navigateur. La règle simple : si tu appelles un setter dans un useEffect, assure-toi qu'il y a une condition d'arrêt ou que la valeur mise à jour n'est pas dans les dépendances.

Conclusion

Avec useState et useEffect, tu as les outils pour construire 80% des composants React que tu rencontreras. useState gère tes données locales et les re-rendus, useEffect synchronise ton composant avec le monde extérieur. Ensemble, ils remplacent tout ce que les class components faisaient avec beaucoup plus de cérémonie.

Les points clés à retenir :

Pour consolider ces bases, va voir la documentation officielle de useState sur react.dev — des dizaines d'exemples interactifs pour chaque cas d'usage.

Et pour pratiquer directement, retrouve tous nos ateliers interactifs GoGoKodo — tu codes dans le navigateur, avec correction automatique et progression guidée.