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.
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.
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>
);
}
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>
);
}
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>
);
}
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
}
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>;
}
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>
);
}
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 };
}
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 ? →
AbortControllerdans 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>;
}
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.