React Hooks : useState, useEffect, useReducer

Front-end 23/03/2026 18:00:00 angularforall.com
React Hooks Usestate Useeffect Usereducer
React Hooks : useState, useEffect, useReducer

Maîtrisez les trois hooks fondamentaux de React : useState, useEffect et useReducer avec des exemples concrets, les pièges à éviter et les bonnes pratiques.

Pourquoi les Hooks ont changé React

Avant React 16.8 (février 2019), pour avoir de l'état ou accéder au cycle de vie dans un composant, vous étiez forcé d'écrire une classe. Le code devenait vite complexe : this.setState, this.componentDidMount, bind(this) partout. La logique métier était éparpillée entre plusieurs méthodes de cycle de vie impossibles à réutiliser.

Les Hooks ont résolu ces problèmes en une seule idée : permettre aux fonctions de gérer l'état et les effets. Plus besoin de classes. Le code devient linéaire, lisible, et la logique métier peut être extraite dans des Custom Hooks réutilisables.

À retenir : Un Hook est simplement une fonction JavaScript qui commence par use et qui "accroche" votre composant aux fonctionnalités internes de React (état, cycle de vie, contexte). React en fournit une dizaine. Vous pouvez créer les vôtres.

Voici la transformation concrète que les Hooks apportent :

Avec une classe (avant) Avec les Hooks (maintenant)
this.state + this.setState() useState()
componentDidMount + componentDidUpdate + componentWillUnmount useEffect()
this.setState avec logique complexe useReducer()
static contextType ou Consumer useContext()
HOC ou Render Props pour réutiliser la logique Custom Hook (useMyLogic())

Dans ce guide, nous allons couvrir en profondeur les trois hooks les plus utilisés : useState, useEffect et useReducer. Ces trois hooks couvrent 90% des besoins d'un composant React.

Prérequis : Connaître les bases de JavaScript (fonctions, tableaux, objets, déstructuration ES6). Avoir créé au moins un composant React fonctionnel simple. Ce guide est conçu pour les développeurs débutants à juniors qui veulent comprendre vraiment comment fonctionnent les Hooks, pas juste les copier-coller.

useState : maîtriser l'état local

useState est le Hook le plus fondamental de React. Il permet à un composant fonctionnel de mémoriser une valeur entre chaque rendu. Sans lui, une variable ordinaire serait réinitialisée à chaque fois que React redessine le composant.

Syntaxe de base

import { useState } from 'react';

function Compteur() {
    // useState retourne un tableau de deux éléments :
    // 1. La valeur actuelle de l'état (ici : 0 au départ)
    // 2. Une fonction pour modifier cet état
    const [count, setCount] = useState(0);

    return (
        <div>
            <p>Compte : {count}</p>
            {/* Appeler setCount déclenche un nouveau rendu */}
            <button onClick={() => setCount(count + 1)}>
                Incrémenter
            </button>
        </div>
    );
}

La syntaxe const [count, setCount] = useState(0) utilise la déstructuration de tableau ES6. React garantit que le premier élément est toujours la valeur courante, et le second toujours la même fonction de mise à jour.

Gérer plusieurs états indépendants

Vous pouvez appeler useState autant de fois que nécessaire. Chaque appel crée un état indépendant.

function FormulaireContact() {
    // Un useState par champ — chaque état est isolé
    const [nom, setNom]       = useState('');
    const [email, setEmail]   = useState('');
    const [message, setMessage] = useState('');
    const [envoye, setEnvoye] = useState(false);

    const handleSubmit = (e) => {
        e.preventDefault();
        // Simuler l'envoi
        console.log({ nom, email, message });
        // Mettre à jour un seul état sans toucher aux autres
        setEnvoye(true);
    };

    // Affichage conditionnel basé sur l'état envoye
    if (envoye) {
        return <p>Message envoyé, merci {nom} !</p>;
    }

    return (
        <form onSubmit={handleSubmit}>
            <input
                value={nom}
                // onChange met à jour l'état à chaque frappe
                onChange={(e) => setNom(e.target.value)}
                placeholder="Votre nom"
            />
            <input
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                placeholder="Votre email"
            />
            <textarea
                value={message}
                onChange={(e) => setMessage(e.target.value)}
            />
            <button type="submit">Envoyer</button>
        </form>
    );
}

useState avec un objet

Il est possible de stocker un objet dans useState. Attention : React ne fait pas de fusion automatique comme this.setState le faisait dans les classes. Vous devez propager manuellement les propriétés existantes avec le spread operator.

function ProfilUtilisateur() {
    // Un seul état pour regrouper des données liées
    const [profil, setProfil] = useState({
        nom: 'Alice',
        age: 28,
        ville: 'Paris'
    });

    const changerVille = (nouvelleVille) => {
        // ✅ CORRECT : propager l'état existant avec ...profil
        // Sans le spread, nom et age seraient effacés !
        setProfil({ ...profil, ville: nouvelleVille });
    };

    const vieillir = () => {
        // ✅ Mise à jour fonctionnelle (voir section suivante)
        setProfil(prev => ({ ...prev, age: prev.age + 1 }));
    };

    return (
        <div>
            <p>{profil.nom}, {profil.age} ans — {profil.ville}</p>
            <button onClick={() => changerVille('Lyon')}>Déménager à Lyon</button>
            <button onClick={vieillir}>Anniversaire</button>
        </div>
    );
}
Conseil : Si les propriétés de votre objet évoluent indépendamment, préférez plusieurs useState séparés. Groupez dans un objet seulement quand les propriétés sont fortement liées (comme les champs d'un formulaire).

Mises à jour d'état : pièges et bonnes pratiques

Les mises à jour avec useState ont des comportements subtils qui piègent presque tous les développeurs juniors. Comprendre ces comportements vous évitera des heures de débogage.

Le piège de l'état figé (stale state)

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

    const incrementerTroisFois = () => {
        // ❌ PROBLÈME : les trois appels lisent la même valeur de count
        // React met les mises à jour en batch — count vaut toujours 0 ici
        setCount(count + 1); // 0 + 1 = 1
        setCount(count + 1); // 0 + 1 = 1 (pas 2 !)
        setCount(count + 1); // 0 + 1 = 1 (pas 3 !)
        // Résultat : count vaudra 1, pas 3
    };

    const incrementerTroisFoisCorrige = () => {
        // ✅ CORRECT : la mise à jour fonctionnelle reçoit la valeur la plus récente
        setCount(prev => prev + 1); // 0 → 1
        setCount(prev => prev + 1); // 1 → 2
        setCount(prev => prev + 1); // 2 → 3
        // Résultat : count vaudra 3 ✓
    };

    return (
        <div>
            <p>Count : {count}</p>
            <button onClick={incrementerTroisFois}>+3 (bogué)</button>
            <button onClick={incrementerTroisFoisCorrige}>+3 (correct)</button>
        </div>
    );
}
Règle d'or : Dès que la nouvelle valeur de l'état dépend de l'ancienne valeur, utilisez toujours la forme fonctionnelle : setState(prev => nouvelleValeur). C'est la seule façon d'être sûr de lire l'état le plus récent.

L'état avec un tableau

Les tableaux en React doivent être traités comme immuables. Ne modifiez jamais directement le tableau existant — créez toujours une nouvelle copie.

function ListeTaches() {
    const [taches, setTaches] = useState([
        { id: 1, texte: 'Apprendre useState', fait: false },
        { id: 2, texte: 'Apprendre useEffect', fait: false },
    ]);

    // ✅ Ajouter : créer un nouveau tableau avec la nouvelle tâche à la fin
    const ajouterTache = (texte) => {
        setTaches(prev => [
            ...prev, // garder toutes les tâches existantes
            { id: Date.now(), texte, fait: false } // ajouter la nouvelle
        ]);
    };

    // ✅ Supprimer : filtrer pour exclure l'élément
    const supprimerTache = (id) => {
        setTaches(prev => prev.filter(tache => tache.id !== id));
    };

    // ✅ Modifier : mapper pour remplacer l'élément ciblé
    const toggleTache = (id) => {
        setTaches(prev => prev.map(tache =>
            tache.id === id
                ? { ...tache, fait: !tache.fait } // inverser le statut
                : tache // garder les autres intacts
        ));
    };

    return (
        <ul>
            {taches.map(tache => (
                <li key={tache.id}>
                    <span style={{ textDecoration: tache.fait ? 'line-through' : 'none' }}>
                        {tache.texte}
                    </span>
                    <button onClick={() => toggleTache(tache.id)}>Toggle</button>
                    <button onClick={() => supprimerTache(tache.id)}>Supprimer</button>
                </li>
            ))}
        </ul>
    );
}
À ne jamais faire : taches.push(nouvelleTache) puis setTaches(taches). React ne détectera pas le changement car la référence du tableau est identique. React compare les références, pas le contenu.

useEffect : gérer les effets de bord

Un effet de bord est toute opération qui interagit avec le monde extérieur au composant : appel API, manipulation du DOM, abonnement à un événement, timer, logging... useEffect est le hook prévu pour exécuter ces opérations de façon contrôlée.

Syntaxe fondamentale

import { useState, useEffect } from 'react';

function ArticleDetail({ articleId }) {
    const [article, setArticle] = useState(null);
    const [chargement, setChargement] = useState(true);
    const [erreur, setErreur] = useState(null);

    // useEffect reçoit deux arguments :
    // 1. Une fonction (l'effet à exécuter)
    // 2. Un tableau de dépendances (quand l'exécuter)
    useEffect(() => {
        // Cette fonction s'exécute après chaque rendu
        // où articleId a changé

        // Réinitialiser l'état avant le nouvel appel
        setChargement(true);
        setErreur(null);

        // Appel API asynchrone
        fetch(`/api/articles/${articleId}`)
            .then(res => {
                if (!res.ok) throw new Error('Article introuvable');
                return res.json();
            })
            .then(data => {
                setArticle(data);
                setChargement(false);
            })
            .catch(err => {
                setErreur(err.message);
                setChargement(false);
            });

    }, [articleId]); // ← Se ré-exécute quand articleId change

    if (chargement) return <p>Chargement...</p>;
    if (erreur)     return <p>Erreur : {erreur}</p>;
    if (!article)   return null;

    return <h1>{article.titre}</h1>;
}

La fonction de nettoyage (cleanup)

Quand un effet crée une ressource persistante (abonnement, timer, event listener), il faut la libérer lorsque le composant se démonte. Pour cela, retournez une fonction de nettoyage depuis votre effet.

function HorlogenTempsReel() {
    const [heure, setHeure] = useState(new Date());

    useEffect(() => {
        // Démarrer un intervalle toutes les secondes
        const intervalId = setInterval(() => {
            setHeure(new Date()); // Mettre à jour l'état
        }, 1000);

        // ✅ Retourner la fonction de nettoyage
        // React l'appellera quand le composant se démonte
        // OU avant de ré-exécuter l'effet (si les dépendances changent)
        return () => {
            clearInterval(intervalId); // Éviter la fuite mémoire
        };
    }, []); // [] = exécuter une seule fois au montage

    return <p>Il est {heure.toLocaleTimeString()}</p>;
}
function EcouteurClavier({ onEscape }) {
    useEffect(() => {
        const handleKeyDown = (event) => {
            // Détecter la touche Échap
            if (event.key === 'Escape') {
                onEscape(); // Appeler la fonction passée en prop
            }
        };

        // Ajouter l'écouteur sur le document entier
        document.addEventListener('keydown', handleKeyDown);

        // Nettoyage : retirer l'écouteur quand le composant disparaît
        return () => {
            document.removeEventListener('keydown', handleKeyDown);
        };
    }, [onEscape]); // Dépend de onEscape

    return null; // Ce composant ne rend rien visuellement
}
Important : Oublier la fonction de nettoyage sur un timer ou un abonnement est l'une des causes les plus fréquentes de fuites mémoire dans les applications React. React vous avertira en mode strict si un effet met à jour l'état d'un composant démonté.

Le tableau de dépendances expliqué

Le deuxième argument de useEffect — le tableau de dépendances — contrôle quand l'effet s'exécute. C'est le concept que les débutants comprennent le moins bien, et qui génère le plus de bugs.

Tableau de dépendances Quand l'effet s'exécute Cas d'usage
Absent (pas de 2e arg) Après chaque rendu Rarement utile, risque de boucle infinie
[] (tableau vide) Une seule fois au montage Fetch initial, abonnements, setup unique
[valeur] Au montage + quand valeur change Fetch conditionnel, synchronisation
[a, b] Au montage + quand a OU b change Dépendances multiples

Éviter les dépendances manquantes

function Recherche({ terme, categorie }) {
    const [resultats, setResultats] = useState([]);

    useEffect(() => {
        // Cet effet utilise terme ET categorie
        // Les deux DOIVENT être dans les dépendances
        fetch(`/api/search?q=${terme}&cat=${categorie}`)
            .then(r => r.json())
            .then(setResultats);

    }, [terme, categorie]); // ✅ Les deux dépendances sont déclarées

    // ❌ Si on écrit [terme] seulement, l'effet ne se ré-exécutera pas
    // quand categorie change — bug silencieux !

    return <ul>{resultats.map(r => <li key={r.id}>{r.nom}</li>)}</ul>;
}

Annuler un fetch quand le composant se démonte

function ArticlesList() {
    const [articles, setArticles] = useState([]);

    useEffect(() => {
        // AbortController permet d'annuler le fetch en cours
        const controller = new AbortController();

        fetch('/api/articles', { signal: controller.signal })
            .then(r => r.json())
            .then(data => {
                // Ne mettre à jour l'état que si le fetch n'a pas été annulé
                setArticles(data);
            })
            .catch(err => {
                // Ignorer l'erreur d'annulation — c'est intentionnel
                if (err.name !== 'AbortError') {
                    console.error('Erreur réseau :', err);
                }
            });

        // Nettoyage : annuler le fetch si le composant se démonte
        return () => controller.abort();
    }, []);

    return <ul>{articles.map(a => <li key={a.id}>{a.titre}</li>)}</ul>;
}
Outil recommandé : Installez le plugin ESLint eslint-plugin-react-hooks. Il détecte automatiquement les dépendances manquantes dans vos tableaux et vous avertit avant que le bug n'arrive en production.

useReducer : état complexe et actions

useReducer est une alternative à useState pour les cas où l'état est complexe : plusieurs sous-valeurs liées, transitions d'état nombreuses, ou logique de mise à jour difficile à lire avec des setXxx enchaînés.

Il s'inspire du pattern Redux : vous définissez un reducer (une fonction pure qui dit "comment passer d'un état à un autre") et vous dispatchez des actions pour le déclencher.

Anatomie de useReducer

import { useReducer } from 'react';

// 1. Définir l'état initial
const etatInitial = {
    articles: [],
    chargement: false,
    erreur: null,
    page: 1,
};

// 2. Écrire le reducer — fonction pure (pas d'effet de bord ici)
// Reçoit l'état actuel + une action, retourne le NOUVEL état
function reducer(etat, action) {
    switch (action.type) {
        case 'CHARGEMENT_DEBUT':
            // Passer en mode chargement, effacer l'erreur précédente
            return { ...etat, chargement: true, erreur: null };

        case 'CHARGEMENT_SUCCES':
            // Stocker les articles, arrêter le chargement
            return { ...etat, chargement: false, articles: action.payload };

        case 'CHARGEMENT_ERREUR':
            // Stocker l'erreur, arrêter le chargement
            return { ...etat, chargement: false, erreur: action.payload };

        case 'PAGE_SUIVANTE':
            // Incrémenter la page
            return { ...etat, page: etat.page + 1 };

        case 'RESET':
            // Revenir à l'état initial
            return etatInitial;

        default:
            // Toujours gérer le cas par défaut !
            throw new Error(`Action inconnue : ${action.type}`);
    }
}

// 3. Utiliser useReducer dans le composant
function ListeArticles() {
    // useReducer(reducer, etatInitial) → [etat, dispatch]
    const [etat, dispatch] = useReducer(reducer, etatInitial);

    useEffect(() => {
        // Dispatcher une action pour signaler le début du chargement
        dispatch({ type: 'CHARGEMENT_DEBUT' });

        fetch(`/api/articles?page=${etat.page}`)
            .then(r => r.json())
            .then(data => {
                // Dispatcher avec un payload (les données reçues)
                dispatch({ type: 'CHARGEMENT_SUCCES', payload: data });
            })
            .catch(err => {
                dispatch({ type: 'CHARGEMENT_ERREUR', payload: err.message });
            });
    }, [etat.page]);

    return (
        <div>
            {etat.chargement && <p>Chargement...</p>}
            {etat.erreur && <p className="text-danger">{etat.erreur}</p>}
            <ul>
                {etat.articles.map(a => <li key={a.id}>{a.titre}</li>)}
            </ul>
            <button onClick={() => dispatch({ type: 'PAGE_SUIVANTE' })}>
                Page suivante
            </button>
        </div>
    );
}

Exemple concret : formulaire multi-étapes

// Formulaire d'inscription en 3 étapes avec useReducer
const etatFormulaire = {
    etape: 1,        // étape courante (1, 2 ou 3)
    prenom: '',
    email: '',
    motDePasse: '',
    accepteConditions: false,
    envoye: false,
};

function reducerFormulaire(etat, action) {
    switch (action.type) {
        case 'CHAMP_CHANGE':
            // Mettre à jour un champ dynamiquement par son nom
            return { ...etat, [action.champ]: action.valeur };

        case 'ETAPE_SUIVANTE':
            return { ...etat, etape: Math.min(etat.etape + 1, 3) };

        case 'ETAPE_PRECEDENTE':
            return { ...etat, etape: Math.max(etat.etape - 1, 1) };

        case 'SOUMETTRE':
            return { ...etat, envoye: true };

        case 'RESET':
            return etatFormulaire;

        default:
            return etat;
    }
}

function FormulaireInscription() {
    const [etat, dispatch] = useReducer(reducerFormulaire, etatFormulaire);

    const changerChamp = (champ) => (e) =>
        dispatch({ type: 'CHAMP_CHANGE', champ, valeur: e.target.value });

    if (etat.envoye) return <p>Bienvenue {etat.prenom} !</p>;

    return (
        <div>
            <p>Étape {etat.etape} / 3</p>

            {etat.etape === 1 && (
                <input
                    value={etat.prenom}
                    onChange={changerChamp('prenom')}
                    placeholder="Prénom"
                />
            )}

            {etat.etape === 2 && (
                <input
                    value={etat.email}
                    onChange={changerChamp('email')}
                    placeholder="Email"
                />
            )}

            {etat.etape === 3 && (
                <input
                    type="password"
                    value={etat.motDePasse}
                    onChange={changerChamp('motDePasse')}
                    placeholder="Mot de passe"
                />
            )}

            <div>
                {etat.etape > 1 && (
                    <button onClick={() => dispatch({ type: 'ETAPE_PRECEDENTE' })}>
                        Retour
                    </button>
                )}
                {etat.etape < 3 && (
                    <button onClick={() => dispatch({ type: 'ETAPE_SUIVANTE' })}>
                        Suivant
                    </button>
                )}
                {etat.etape === 3 && (
                    <button onClick={() => dispatch({ type: 'SOUMETTRE' })}>
                        S'inscrire
                    </button>
                )}
            </div>
        </div>
    );
}
useState vs useReducer : Préférez useReducer quand vous avez 3+ états liés qui évoluent ensemble, ou quand la logique de transition devient difficile à lire avec plusieurs setState enchaînés. Pour un état simple et indépendant, useState reste plus lisible.

Les règles des Hooks à ne jamais violer

React impose deux règles absolues pour que les Hooks fonctionnent correctement. Ces règles ne sont pas optionnelles — les violer provoque des bugs imprévisibles et difficiles à diagnostiquer.

Règle 1 : Appeler les Hooks uniquement au niveau supérieur

// ❌ INTERDIT : Hook dans une condition
function ComposantBogue({ estConnecte }) {
    if (estConnecte) {
        // React ne peut pas garantir l'ordre des appels de Hooks
        const [profil, setProfil] = useState(null); // VIOLATION
    }
    // ...
}

// ❌ INTERDIT : Hook dans une boucle
function ListeItems({ items }) {
    items.forEach(item => {
        const [actif, setActif] = useState(false); // VIOLATION
    });
    // ...
}

// ✅ CORRECT : Hooks toujours au niveau racine de la fonction
function ComposantCorrect({ estConnecte }) {
    // Tous les hooks en haut, sans condition
    const [profil, setProfil] = useState(null);
    const [actif, setActif] = useState(false);

    // La condition s'applique sur la logique, pas sur le Hook
    useEffect(() => {
        if (estConnecte) {
            fetch('/api/profil').then(r => r.json()).then(setProfil);
        }
    }, [estConnecte]);

    // ...
}

Règle 2 : Appeler les Hooks uniquement dans des composants React ou des Custom Hooks

// ❌ INTERDIT : Hook dans une fonction JavaScript normale
function calculerScore(points) {
    const [score, setScore] = useState(0); // VIOLATION — pas un composant
    return score + points;
}

// ❌ INTERDIT : Hook dans un callback ou gestionnaire d'événement
function MonComposant() {
    document.addEventListener('click', () => {
        const [clicked, setClicked] = useState(false); // VIOLATION
    });
}

// ✅ CORRECT : Hook dans un composant React fonctionnel
function ScoreBoard({ points }) {
    const [score, setScore] = useState(0); // OK — dans un composant
    return <p>Score : {score}</p>;
}

// ✅ CORRECT : Hook dans un Custom Hook (commence par "use")
function useScore(pointsInitiaux) {
    const [score, setScore] = useState(pointsInitiaux); // OK — Custom Hook
    const incrementer = () => setScore(prev => prev + 1);
    return { score, incrementer };
}
Pourquoi ces règles ? React identifie chaque Hook par son ordre d'appel. Si vous appelez des Hooks conditionnellement, l'ordre peut changer entre les rendus, et React perd la correspondance entre un Hook et son état mémorisé.

Erreurs fréquentes des juniors

Voici les 5 erreurs que les développeurs débutants font systématiquement avec les Hooks. Chaque erreur est accompagnée de sa correction.

Erreur 1 — Mutater l'état directement

// ❌ Mutation directe — React ne détecte pas le changement
const [user, setUser] = useState({ nom: 'Alice', age: 28 });

const handleBirthday = () => {
    user.age = 29; // ← Muter l'objet existant
    setUser(user); // React voit la même référence → pas de re-rendu !
};

// ✅ Créer un nouvel objet à chaque fois
const handleBirthdayCorrige = () => {
    setUser(prev => ({ ...prev, age: prev.age + 1 })); // Nouvelle référence
};

Erreur 2 — Boucle infinie avec useEffect

// ❌ Boucle infinie : l'effet met à jour un état présent dans ses dépendances
function ComposantInfini() {
    const [data, setData] = useState([]);

    useEffect(() => {
        fetch('/api/data')
            .then(r => r.json())
            .then(result => setData(result)); // Met à jour data
    }, [data]); // data est dans les dépendances → re-fetch à l'infini !
}

// ✅ Correct : ne pas inclure l'état que l'on modifie dans les dépendances
function ComposantCorrige() {
    const [data, setData] = useState([]);

    useEffect(() => {
        fetch('/api/data')
            .then(r => r.json())
            .then(result => setData(result));
    }, []); // Fetch une seule fois au montage
}

Erreur 3 — Utiliser une valeur d'état obsolète dans un callback

// ❌ count est "capturé" à la valeur du premier rendu (0)
function CompteurTimer() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        const id = setInterval(() => {
            console.log(count); // Affiche toujours 0 !
            setCount(count + 1); // Bug : toujours 0 + 1 = 1
        }, 1000);
        return () => clearInterval(id);
    }, []); // [] = pas de re-abonnement, count reste figé à 0
}

// ✅ Utiliser la forme fonctionnelle pour lire la valeur fraîche
function CompteurTimerCorrige() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        const id = setInterval(() => {
            setCount(prev => prev + 1); // prev est toujours la valeur actuelle
        }, 1000);
        return () => clearInterval(id);
    }, []); // OK car on n'utilise plus count directement
}

Erreur 4 — Oublier le nettoyage d'un abonnement

// ❌ Fuite mémoire : l'abonnement continue après démontage du composant
function Notifications() {
    const [notifs, setNotifs] = useState([]);

    useEffect(() => {
        const ws = new WebSocket('wss://api.example.com/notifs');
        ws.onmessage = (event) => {
            setNotifs(prev => [...prev, JSON.parse(event.data)]);
            // Si le composant est démonté, cette mise à jour génère une erreur
        };
        // ← Pas de cleanup !
    }, []);
}

// ✅ Fermer le WebSocket au démontage
function NotificationsCorrige() {
    const [notifs, setNotifs] = useState([]);

    useEffect(() => {
        const ws = new WebSocket('wss://api.example.com/notifs');
        ws.onmessage = (event) => {
            setNotifs(prev => [...prev, JSON.parse(event.data)]);
        };

        return () => ws.close(); // Fermeture propre
    }, []);
}

Erreur 5 — Mettre une fonction dans les dépendances sans useCallback

// ❌ onFetch est recréée à chaque rendu → useEffect se ré-exécute en boucle
function Composant({ userId }) {
    // Cette fonction est recréée à chaque rendu
    const onFetch = () => fetch(`/api/users/${userId}`);

    useEffect(() => {
        onFetch().then(r => r.json()).then(console.log);
    }, [onFetch]); // onFetch change à chaque rendu → boucle !
}

// ✅ Option 1 : déplacer la fonction dans l'effet
function ComposantCorrige1({ userId }) {
    useEffect(() => {
        const fetchUser = () => fetch(`/api/users/${userId}`);
        fetchUser().then(r => r.json()).then(console.log);
    }, [userId]); // Dépendre de userId uniquement
}

// ✅ Option 2 : mémoriser la fonction avec useCallback
function ComposantCorrige2({ userId }) {
    const fetchUser = useCallback(
        () => fetch(`/api/users/${userId}`),
        [userId] // Recréer seulement quand userId change
    );

    useEffect(() => {
        fetchUser().then(r => r.json()).then(console.log);
    }, [fetchUser]);
}

Comparaison et quand utiliser quoi

Maintenant que vous maîtrisez les trois Hooks, voici un guide de décision pour choisir le bon outil selon votre situation.

Situation Hook recommandé Pourquoi
Compteur, toggle, chaîne simple useState Simple, lisible, suffisant
Champs de formulaire indépendants useState × N Chaque champ évolue séparément
Formulaire complexe multi-étapes useReducer Transitions d'état structurées
Appel API / fetch useEffect + useState Effet de bord déclenché au montage
Chargement/erreur/données groupés useReducer 3 états liés → reducer clair
Abonnement WebSocket / EventSource useEffect avec cleanup Nettoyage obligatoire au démontage
Synchroniser le titre de la page useEffect Effet de bord DOM externe
Panier e-commerce avec actions multiples useReducer ADD, REMOVE, UPDATE, CLEAR = actions claires

Checklist avant de coder

  • Mon état est-il simple et indépendant ? → useState
  • J'ai 3+ états liés ou des transitions complexes ? → useReducer
  • Mon effet crée une ressource persistante ? → prévoir le cleanup
  • Toutes mes dépendances sont dans le tableau ? → installer eslint-plugin-react-hooks
  • Je mets à jour l'état depuis une valeur précédente ? → forme fonctionnelle prev => ...
  • Je ne mute jamais directement l'état → spread operator ou nouveau tableau
  • Mes Hooks sont tous appelés au niveau supérieur → pas de conditions
  • Je fetch des données ? → AbortController dans le cleanup
  • J'ai du code répété entre composants ? → extraire dans un Custom Hook
  • J'ai testé les états de chargement et d'erreur dans l'UI ?

Prochaine étape : les Custom Hooks

Une fois à l'aise avec ces trois Hooks fondamentaux, la prochaine étape naturelle est de créer vos propres Custom Hooks. Un Custom Hook est simplement une fonction qui commence par use et qui combine d'autres Hooks pour encapsuler une logique réutilisable.

// Exemple de Custom Hook : useFetch
// Encapsule le trio useState + useEffect pour les appels API
function useFetch(url) {
    const [data, setData]         = useState(null);
    const [chargement, setChargement] = useState(true);
    const [erreur, setErreur]     = useState(null);

    useEffect(() => {
        const controller = new AbortController();
        setChargement(true);

        fetch(url, { signal: controller.signal })
            .then(r => {
                if (!r.ok) throw new Error(`HTTP ${r.status}`);
                return r.json();
            })
            .then(data => { setData(data); setChargement(false); })
            .catch(err => {
                if (err.name !== 'AbortError') {
                    setErreur(err.message);
                    setChargement(false);
                }
            });

        return () => controller.abort();
    }, [url]);

    return { data, chargement, erreur };
}

// Utilisation dans n'importe quel composant
function ArticlesPage() {
    // Une seule ligne remplace 15 lignes de useEffect + useState
    const { data: articles, chargement, erreur } = useFetch('/api/articles');

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

    return <ul>{articles.map(a => <li key={a.id}>{a.titre}</li>)}</ul>;
}
Conclusion : Les Hooks ne sont pas juste une nouvelle syntaxe — ils représentent une façon de penser différente. Avec useState pour l'état local, useEffect pour synchroniser avec le monde extérieur, et useReducer pour orchestrer des transitions complexes, vous avez tous les outils pour construire des composants React robustes, lisibles et maintenables.

Partager