Maîtriser React.js : Construire des Interfaces Utilisateur Modernes et Réactives
Maîtriser React.js : Construire des Interfaces Utilisateur Modernes et Réactives

Gérer l'État avec les Hooks (useState, useEffect)

Bienvenue dans cette leçon dédiée à la gestion de l'état dans les composants fonctionnels React grâce aux Hooks. Dans le cadre de notre cours "Maîtriser React.js : Construire des Interfaces Utilisateur Modernes et Réactives", comprendre comment gérer les données qui changent au fil du temps est absolument fondamental pour bâtir des applications dynamiques et réactives.

Avant l'introduction des Hooks, la gestion de l'état et des effets de bord était principalement l'apanage des composants de classe. Les Hooks, introduits avec React 16.8, ont révolutionné la manière dont nous écrivons des composants en nous permettant d'utiliser des fonctionnalités d'état et de cycle de vie directement dans des composants fonctionnels, rendant notre code plus concis, plus lisible et plus facile à tester.

Cette leçon se concentrera sur les deux Hooks les plus fondamentaux et les plus couramment utilisés : useState pour ajouter de l'état aux composants, et useEffect pour gérer les effets de bord (side effects) tels que les requêtes API, la manipulation directe du DOM ou les abonnements.


1. Comprendre l'État en React

Qu'est-ce que l'État ?

En React, l'état (state) est un ensemble de données qui peut changer au fil du temps et qui influence le rendu de vos composants. C'est la source de vérité pour les données dynamiques de votre application. Chaque fois que l'état d'un composant change, React réagit en re-rendant le composant pour refléter ces nouvelles données.

  • Exemples d'état :
    • La valeur d'un champ de saisie.
    • Si un menu déroulant est ouvert ou fermé.
    • La liste des articles récupérés depuis une API.
    • Le thème actuel (clair/sombre).
    • Le nombre d'articles dans un panier d'achat.

Pourquoi l'État est-il Important ?

L'état est ce qui rend les interfaces utilisateur interactives et dynamiques. Sans état, vos composants seraient purement statiques, affichant toujours les mêmes informations. La capacité de modifier et de réagir aux changements de données est au cœur de la création d'expériences utilisateur riches.


2. Le Hook useState : Gérer l'État Simple

Qu'est-ce que useState ?

useState est le Hook fondamental qui vous permet d'ajouter une variable d'état à un composant fonctionnel. Il gère une seule valeur d'état (qui peut être un nombre, une chaîne de caractères, un booléen, un objet, un tableau, etc.) et fournit une fonction pour mettre à jour cette valeur.

Comment l'utiliser ?

  1. Importation : Vous devez importer useState depuis React.

    import React, { useState } from 'react';
    
  2. Déclaration : À l'intérieur de votre composant fonctionnel, vous appelez useState en lui passant la valeur initiale de votre état. useState retourne une paire de valeurs dans un tableau :

    • La valeur actuelle de l'état.
    • Une fonction pour mettre à jour cette valeur.

    La convention est d'utiliser la déstructuration de tableau pour nommer ces deux éléments.

    const [nomDeLEtat, setNomDeLEtat] = useState(valeurInitiale);
    
    • nomDeLEtat : Votre variable d'état. C'est elle que vous utiliserez dans votre JSX.
    • setNomDeLEtat : La fonction pour mettre à jour nomDeLEtat. Par convention, on préfixe cette fonction par set.
    • valeurInitiale : La valeur que prendra nomDeLEtat lors du premier rendu du composant.
  3. Lecture de l'état : Accédez simplement à la variable nomDeLEtat dans votre JSX ou votre logique.

  4. Mise à jour de l'état : Appelez la fonction setNomDeLEtat avec la nouvelle valeur. Lorsque setNomDeLEtat est appelée, React re-rend le composant avec la nouvelle valeur.

    // Mettre à jour avec une nouvelle valeur directe
    setNomDeLEtat(nouvelleValeur);
    
    // Mettre à jour avec une fonction de callback (si la nouvelle valeur dépend de l'ancienne)
    setNomDeLEtat(prevValeur => prevValeur + 1);
    

    Il est fortement recommandé d'utiliser la fonction de callback si votre nouvelle valeur d'état dépend de l'état précédent, car cela garantit que vous travaillez avec la valeur la plus à jour (utile dans les mises à jour asynchrones ou groupées).

Exemple Pratique de useState : Un Compteur Simple

import React, { useState } from 'react';

function Compteur() {
  // Déclaration d'une variable d'état 'count' avec une valeur initiale de 0
  // 'count' est la valeur actuelle, 'setCount' est la fonction pour la modifier
  const [count, setCount] = useState(0);

  // Fonction pour incrémenter le compteur
  const handleIncrement = () => {
    // setCount(count + 1); // Ceci fonctionne aussi, mais moins sécurisé pour des updates rapides
    setCount(prevCount => prevCount + 1); // Utilisation d'une fonction de callback
  };

  // Fonction pour décrémenter le compteur
  const handleDecrement = () => {
    setCount(prevCount => prevCount - 1);
  };

  // Fonction pour réinitialiser le compteur
  const handleReset = () => {
    setCount(0);
  };

  return (
    <div>
      <h2>Compteur</h2>
      <p>Valeur actuelle : {count}</p>
      <button onClick={handleIncrement}>Incrémenter</button>
      <button onClick={handleDecrement}>Décrémenter</button>
      <button onClick={handleReset}>Réinitialiser</button>
    </div>
  );
}

export default Compteur;

Explication du code :

  • Nous importons useState de React.
  • Dans le composant Compteur, nous appelons useState(0) pour déclarer une variable d'état count initialisée à 0. setCount est la fonction qui nous permettra de modifier count.
  • Les fonctions handleIncrement, handleDecrement et handleReset appellent setCount pour mettre à jour la valeur de count.
  • Notez l'utilisation de prevCount => prevCount + 1 pour setCount. Cette forme est préférable car elle garantit que la mise à jour de l'état utilise la valeur la plus récente de count, ce qui est crucial si plusieurs mises à jour peuvent se produire en même temps ou si React regroupe les mises à jour.
  • Lorsque setCount est appelée, React déclenche un nouveau rendu du composant Compteur, et la nouvelle valeur de count est affichée.

3. Le Hook useEffect : Gérer les Effets de Bord

Qu'est-ce que useEffect ?

useEffect est le Hook qui vous permet d'effectuer des "effets de bord" (side effects) dans les composants fonctionnels. Les effets de bord sont toutes les opérations qui interagissent avec le monde extérieur à votre composant React, ou qui sont des opérations asynchrones.

  • Exemples d'effets de bord :
    • Récupérer des données depuis une API (fetch, axios).
    • Manipuler directement le DOM (par exemple, changer le titre du document).
    • Mettre en place ou nettoyer des abonnements (timers, écouteurs d'événements).
    • Synchroniser avec des systèmes externes.

useEffect peut être vu comme une combinaison des méthodes de cycle de vie des composants de classe (componentDidMount, componentDidUpdate, componentWillUnmount), mais avec une approche plus flexible et déclarative.

La Syntaxe de useEffect

Le Hook useEffect prend deux arguments :

  1. Une fonction de callback : C'est là que vous placez votre code d'effet de bord.
  2. Un tableau de dépendances (optionnel) : Ce tableau contrôle quand l'effet doit être ré-exécuté.
useEffect(() => {
  // Votre code d'effet de bord ici

  return () => {
    // (Optionnel) Fonction de nettoyage
    // Exécutée avant la prochaine exécution de l'effet
    // ou lors du démontage du composant
  };
}, [dépendance1, dépendance2]); // Le tableau de dépendances

Les Cas d'Utilisation de useEffect (avec le tableau de dépendances)

La manière dont vous définissez le tableau de dépendances a un impact majeur sur le moment où votre effet est exécuté.

a) Sans tableau de dépendances (Exécuté à chaque rendu)

useEffect(() => {
  console.log("Cet effet s'exécute à chaque rendu du composant.");
});
  • Quand l'utiliser ? Très rarement, car cela peut créer des boucles infinies ou des performances médiocres. Utilisation très spécifique pour des synchronisations avec le DOM après chaque mise à jour.

b) Avec un tableau de dépendances vide ([]) (Exécuté une seule fois après le montage)

useEffect(() => {
  console.log("Cet effet s'exécute une seule fois après le montage du composant.");
  // Idéal pour les requêtes API initiales, la configuration d'écouteurs d'événements globaux, etc.
}, []);
  • Quand l'utiliser ? Similaire à componentDidMount. L'effet ne se déclenchera qu'une seule fois après le premier rendu du composant. C'est parfait pour charger des données initiales ou configurer des abonnements qui n'ont pas besoin d'être mis à jour.

c) Avec des dépendances spécifiques ([dep1, dep2]) (Exécuté au montage et quand les dépendances changent)

useEffect(() => {
  console.log("Cet effet s'exécute quand 'propA' ou 'stateB' changent.");
  // Idéal pour re-charger des données en fonction de props ou de l'état
}, [propA, stateB]);
  • Quand l'utiliser ? Similaire à componentDidMount et componentDidUpdate combinés. L'effet s'exécutera au montage et chaque fois que l'une des valeurs spécifiées dans le tableau de dépendances change. Si une dépendance est un objet ou un tableau, assurez-vous qu'elle est comparable par référence (ou utilisez JSON.stringify pour une comparaison profonde, bien que ce soit rarement la meilleure solution).

d) Avec une fonction de nettoyage (Exécuté avant la prochaine exécution de l'effet ou au démontage)

La fonction de retour (cleanup function) dans useEffect est cruciale pour annuler les effets de bord qui pourraient continuer à s'exécuter ou à consommer des ressources après que le composant ait été démonté ou que les dépendances aient changé.

useEffect(() => {
  console.log("Effet déclenché.");
  const timerId = setInterval(() => {
    console.log("Intervalle en cours...");
  }, 1000);

  // Fonction de nettoyage
  return () => {
    console.log("Nettoyage de l'effet...");
    clearInterval(timerId); // Annule le timer
  };
}, []); // S'exécute une seule fois au montage et nettoie au démontage
  • Quand l'utiliser ? Pour nettoyer des abonnements, annuler des requêtes en cours, supprimer des écouteurs d'événements, ou toute ressource allouée par l'effet. La fonction de nettoyage s'exécute :
    • Avant que l'effet ne se re-déclenche (si les dépendances changent).
    • Lorsque le composant est démonté de l'arbre DOM.

Exemple Pratique de useEffect : Titre Dynamique et Requête de Données

import React, { useState, useEffect } from 'react';

function ProfilUtilisateur({ userId }) {
  const [userData, setUserData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // Effet 1: Mettre à jour le titre du document
  useEffect(() => {
    document.title = userData ? `Profil de ${userData.name}` : 'Chargement du profil...';

    // Pas de fonction de nettoyage ici car la mise à jour est simple et non persistante
  }, [userData]); // Se re-déclenche quand 'userData' change

  // Effet 2: Récupérer les données de l'utilisateur
  useEffect(() => {
    // Réinitialiser l'état de chargement et d'erreur à chaque changement de userId
    setLoading(true);
    setError(null);
    setUserData(null); // Optionnel: pour vider les anciennes données avant le chargement

    const fetchUserData = async () => {
      try {
        const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
        if (!response.ok) {
          throw new Error(`Erreur HTTP: ${response.status}`);
        }
        const data = await response.json();
        setUserData(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    // Pour éviter les bugs de "race condition" si le composant est démonté
    // avant que la requête ne soit terminée, ou si userId change rapidement.
    // Cette partie est une bonne pratique pour les effets asynchrones.
    let isMounted = true;
    fetchUserData().then(() => {
      if (!isMounted) {
        // Optionnel: console.log(`Requête pour userId ${userId} terminée après démontage.`);
      }
    });

    // Fonction de nettoyage: S'exécute si 'userId' change ou si le composant est démonté
    return () => {
      isMounted = false; // Permet de savoir si le composant est toujours monté
      // Ici, on pourrait aussi annuler la requête si l'API le permet.
      console.log(`Nettoyage pour userId: ${userId} (avant la prochaine exécution ou démontage)`);
    };
  }, [userId]); // Dépend de 'userId'. Se re-déclenche si 'userId' change.

  if (loading) return <p>Chargement du profil de l'utilisateur...</p>;
  if (error) return <p style={{ color: 'red' }}>Erreur: {error}</p>;
  if (!userData) return <p>Aucun utilisateur trouvé.</p>;

  return (
    <div>
      <h3>Profil de {userData.name}</h3>
      <p>Email: {userData.email}</p>
      <p>Téléphone: {userData.phone}</p>
      <p>Ville: {userData.address.city}</p>
    </div>
  );
}

// Composant Parent pour tester ProfilUtilisateur avec différents IDs
function App() {
  const [currentUserId, setCurrentUserId] = useState(1);

  return (
    <div>
      <h1>Gestion des Profils Utilisateurs</h1>
      <button onClick={() => setCurrentUserId(prevId => Math.max(1, prevId - 1))}>
        Utilisateur Précédent
      </button>
      <button onClick={() => setCurrentUserId(prevId => prevId + 1)}>
        Utilisateur Suivant
      </button>
      <p>Affichage de l'utilisateur ID: {currentUserId}</p>
      <ProfilUtilisateur userId={currentUserId} />
    </div>
  );
}

export default App;

Explication du code :

  1. useEffect pour le titre du document :

    • Il utilise document.title pour changer le titre de l'onglet du navigateur.
    • Son tableau de dépendances est [userData]. Cela signifie qu'il s'exécutera une première fois au montage du composant, puis à chaque fois que la valeur de userData changera. C'est parfait pour maintenir le titre à jour en fonction des données chargées.
  2. useEffect pour la récupération de données :

    • Il contient une fonction asynchrone fetchUserData qui utilise l'API fetch pour récupérer des données utilisateur.
    • Il gère les états de chargement (loading), d'erreur (error) et des données (userData) à l'aide de useState.
    • Son tableau de dépendances est [userId]. Cela garantit que la requête est re-déclenchée uniquement si userId change. Si userId ne change pas, même si le composant est re-rendu pour d'autres raisons, l'effet de fetch ne sera pas exécuté inutilement.
    • La fonction de nettoyage (return () => { ... }) est présente. Dans cet exemple, elle sert principalement à signaler que le composant n'est plus monté, ce qui est une bonne pratique pour éviter de tenter de modifier l'état d'un composant qui n'est plus dans l'arbre (les "memory leaks" et les avertissements React). Pour des APIs qui supportent l'annulation de requêtes (comme AbortController avec fetch), ce serait l'endroit idéal pour annuler la requête.

4. Bonnes Pratiques et Pièges Courants

Avec useState :

  • Immutabilité de l'état : Lorsque vous mettez à jour un état qui est un objet ou un tableau, vous devez toujours créer une nouvelle instance de cet objet ou tableau. Ne modifiez jamais directement l'objet ou le tableau existant.
    // Incorrect: Modifie l'état directement
    // const [user, setUser] = useState({ name: 'Alice', age: 30 });
    // user.age = 31; // NE FAITES PAS ÇA ! React ne détectera pas le changement.
    
    // Correct: Crée un nouvel objet
    setUserData(prevUser => ({ ...prevUser, age: 31 }));
    
    // Correct pour un tableau:
    // const [items, setItems] = useState(['pomme', 'poire']);
    // setItems(prevItems => [...prevItems, 'orange']); // Ajoute un élément
    
  • Utiliser la fonction de callback pour les mises à jour asynchrones : Si votre nouvelle valeur d'état dépend de l'état précédent (comme dans setCount(prevCount => prevCount + 1)), utilisez la forme fonctionnelle de setState. C'est essentiel lorsque des mises à jour rapides ou asynchrones peuvent se produire, car React peut regrouper les appels à setState.

Avec useEffect :

  • La clarté du tableau de dépendances est cruciale :
    • Si vous laissez le tableau vide ([]), l'effet ne se déclenchera qu'une fois au montage.
    • Si vous omettez le tableau, l'effet se déclenchera à chaque rendu, ce qui est rarement ce que vous voulez.
    • Si vous incluez des variables dans le tableau, assurez-vous que toutes les variables utilisées à l'intérieur de votre effet et qui proviennent du scope extérieur (props, state, fonctions déclarées dans le composant) sont listées comme dépendances, sauf si elles sont garanties de ne pas changer (fonctions utilitaires pures, constantes). React vous avertira souvent si vous oubliez une dépendance.
  • Attention aux boucles infinies : Si votre effet met à jour une variable d'état qui est elle-même une dépendance de cet effet, vous pourriez créer une boucle infinie.
    // Piège ! Boucle infinie potentielle si 'count' est mis à jour à l'intérieur
    useEffect(() => {
      // ... logique qui modifie 'count'
    }, [count]);
    
    Pour éviter cela, assurez-vous que les dépendances sont stables, ou utilisez la forme fonctionnelle de setState si l'état actuel est nécessaire pour la mise à jour.
  • Nettoyer vos effets : Si votre effet crée des abonnements (timers, écouteurs d'événements, websockets), il est impératif de retourner une fonction de nettoyage pour annuler ces abonnements. Ne pas le faire peut entraîner des fuites de mémoire (memory leaks) et des comportements inattendus.
  • Les "Rules of Hooks" :
    • N'appelez les Hooks qu'au niveau supérieur de votre fonction React. Ne les appelez pas à l'intérieur de boucles, de conditions, ou de fonctions imbriquées.
    • N'appelez les Hooks qu'à partir de fonctions React fonctionnelles. Ne les appelez pas à partir de fonctions JavaScript ordinaires.

Conclusion

Les Hooks useState et useEffect sont les piliers de la gestion de l'état et des effets de bord dans les composants fonctionnels React.

  • useState vous équipe pour ajouter et gérer l'état local d'un composant de manière simple et déclarative. Il est votre allié pour toutes les données qui peuvent varier et influencer directement le rendu de votre UI.
  • useEffect vous permet de gérer les interactions avec le monde extérieur à votre composant, de la récupération de données à la manipulation du DOM, en passant par la gestion de ressources. Sa flexibilité, grâce au tableau de dépendances et à la fonction de nettoyage, en fait un outil puissant pour maîtriser le cycle de vie de vos effets.

En maîtrisant ces deux Hooks, vous débloquerez une grande partie du potentiel de React. Ils rendent le code plus modulaire, plus facile à raisonner et plus réutilisable, propulsant vos compétences en développement React à un niveau supérieur. La pratique est la clé : n'hésitez pas à expérimenter avec des exemples et à construire vos propres composants pour solidifier votre compréhension.