useState et useEffect : les utiliser correctement
1. Aperçu
useState et useEffect sont les deux hooks les plus basiques de React, mais c’est aussi là que naissent la plupart des bugs : des rendus infinis, un state qui ne se met pas à jour comme prévu, des effects qui s’exécutent à répétition inutilement. Cet article explore le fonctionnement réel des deux hooks, les bonnes pratiques et les erreurs courantes à éviter. Après cette lecture, vous serez plus confiant pour décider « faut-il useEffect ici ? » — une question que beaucoup de développeurs React se posent encore et se trompent.
2. useState : les bases
useState retourne une paire [value, setter]. Chaque fois que vous appelez le setter, React planifie un rendu du composant avec la nouvelle valeur.
import { useState } from "react";
function CustomerCounter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Nombre de clients : {count}</p>
<button onClick={() => setCount(count + 1)}>Ajouter un client</button>
</div>
);
}
useState(0)initialisecount = 0seulement au premier rendu.setCount(count + 1)crée un nouveau rendu aveccountaugmenté de 1.countdans la closure du rendu actuel est immuable — il ne change pas entre deux rendus.
Remarque : Appeler
setCountne change pas immédiatement la valeur decountdans le même rendu. React regroupe les mises à jour et effectue le rendu ensuite.
3. useState : mettre à jour en fonction du state précédent
Si vous appelez le setter plusieurs fois dans le même gestionnaire d’événement sur la base de count (variable « gelée »), le résultat sera incorrect.
function handleTripleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// Résultat : count n'augmente que de 1, pas de 3
}
Utilisez la mise à jour fonctionnelle pour obtenir la valeur la plus récente :
function handleTripleClick() {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
// Résultat : count augmente correctement de 3
}
- Quand vous passez une fonction au setter, React garantit que
prevest toujours la valeur d’état la plus récente au moment du traitement, et non la valeur capturée dans l’ancienne closure. - Règle : si le nouvel état dépend de l’ancien état, utilisez toujours la mise à jour fonctionnelle.
4. useState : initialisation différée
Si la valeur d’initialisation nécessite un calcul coûteux, ne l’appelez pas directement — passez une fonction à useState pour qu’elle ne s’exécute qu’une fois.
function parseInitialOrders(raw) {
console.log("Analyse des données de commande...");
return JSON.parse(raw);
}
function OrderList({ rawOrders }) {
// FAUX : parseInitialOrders s'exécute à chaque rendu
// const [orders, setOrders] = useState(parseInitialOrders(rawOrders));
// BON : s'exécute seulement au premier rendu
const [orders, setOrders] = useState(() => parseInitialOrders(rawOrders));
return <p>Nombre de commandes : {orders.length}</p>;
}
- La mauvaise approche appelle
parseInitialOrdersà chaque rendu, bien que React n’utilise que le résultat du premier. - La bonne approche passe une référence de fonction ; React ne l’invoque que lors du montage initial.
5. useEffect : les bases
useEffect exécute un effet secondaire après le rendu du composant (après la mise à jour du DOM).
import { useEffect, useState } from "react";
function PageTitleUpdater({ orderCount }) {
useEffect(() => {
document.title = `Vous avez ${orderCount} commandes`;
});
return <p>Commandes actuelles : {orderCount}</p>;
}
- Sans dependency array (la deuxième partie), cet effect s’exécute après chaque rendu — au montage comme à la mise à jour.
document.titleest un effet secondaire typique : il interagit avec le monde extérieur à React (DOM, API), ce n’est pas du calcul de valeur pour le rendu.
6. useEffect : le tableau de dépendances
Le tableau de dépendances détermine quand l’effect s’exécute. Il y a 3 cas à bien comprendre.
6.1. Sans tableau de dépendances
useEffect(() => {
console.log("S'exécute après CHAQUE rendu");
});
6.2. Tableau de dépendances vide []
useEffect(() => {
console.log("S'exécute UNE SEULE FOIS après le montage");
}, []);
6.3. Tableau de dépendances avec valeurs
function CustomerProfile({ customerId }) {
const [customer, setCustomer] = useState(null);
useEffect(() => {
fetchCustomer(customerId).then(setCustomer);
}, [customerId]);
return <p>{customer?.name ?? "Chargement..."}</p>;
}
- L’effect ne s’exécute à nouveau que quand
customerIdchange entre deux rendus (comparaison viaObject.is). - Si vous oubliez
customerIddans le tableau, l’effect utilisera l’ancienne valeur decustomerId(stale closure) — va chercher le mauvais client quand la prop change.
Remarque : Le plugin ESLint
eslint-plugin-react-hooksavec la règleexhaustive-depsvous avertira si vous oubliez une dépendance. Ne désactivez pas cette règle sauf si vous comprenez bien les conséquences.
7. useEffect : la fonction de nettoyage
Quand un effect crée une subscription, un timer ou un écouteur d’événement, vous devez retourner une fonction de nettoyage pour éviter les fuites.
function OrderStatusListener({ orderId }) {
const [status, setStatus] = useState("pending");
useEffect(() => {
const subscription = subscribeToOrderStatus(orderId, setStatus);
return () => {
subscription.unsubscribe();
};
}, [orderId]);
return <p>État : {status}</p>;
}
- La fonction de nettoyage s’exécute avant que l’effect ne s’exécute à nouveau (quand
orderIdchange) et lors du démontage du composant. - Sans nettoyage, chaque fois que
orderIdchange, une nouvelle subscription se crée — finalementsetStatusest appelé depuis plusieurs anciennes subscriptions, causant des mises à jour incorrectes ou des fuites mémoire.
Ordre d'exécution quand orderId passe de 1 à 2 :
1. nettoyage de l'effect avec orderId = 1
2. nouvel effect s'exécute avec orderId = 2
8. Les erreurs courantes
8.1. Boucles infinies dues à des dépendances manquantes
function BadExample() {
const [data, setData] = useState({ count: 0 });
useEffect(() => {
// nouvel objet à chaque rendu -> l'effect voit toujours un 'changement' si vous mettez data dans les deps
setData({ count: data.count + 1 }); // cause un re-rendu -> l'effect s'exécute à nouveau -> infini
}, [data]);
return <p>{data.count}</p>;
}
Corrigez avec une mise à jour fonctionnelle et isolez la condition d’arrêt, ou supprimez complètement useEffect si cette logique n’a pas besoin d’effet secondaire réel.
8.2. Stale closure dans setTimeout/setInterval
function Countdown() {
const [seconds, setSeconds] = useState(10);
useEffect(() => {
const id = setInterval(() => {
setSeconds((prev) => prev - 1); // utilise une mise à jour fonctionnelle, ne pas utiliser seconds directement
}, 1000);
return () => clearInterval(id);
}, []); // deps vide car on ne lit pas seconds directement dans l'effect
return <p>{seconds}</p>;
}
- Si vous écrivez
setSeconds(seconds - 1)au lieu d’une mise à jour fonctionnelle, la closure « se souviendra » toujours deseconds = 10de la première exécution de l’effect — le compte à rebours se bloque sur une mauvaise valeur.
8.3. Utiliser useEffect pour calculer une valeur dérivée
// FAUX : redondant, provoque un rendu supplémentaire
function OrderSummaryBad({ items }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price, 0));
}, [items]);
return <p>Total : {total}</p>;
}
// BON : calculer directement pendant le rendu
function OrderSummaryGood({ items }) {
const total = items.reduce((sum, i) => sum + i.price, 0);
return <p>Total : {total}</p>;
}
La version « FAUX » coûte un rendu supplémentaire : rendu avec l’ancien total, l’effect s’exécute, setTotal déclenche un deuxième rendu avec la bonne valeur. La version « BON » calcule directement pendant le rendu, pas besoin d’effect, pas besoin d’état secondaire.
9. Quand NE PAS utiliser useEffect
Remarque :
useEffectest réservé à la synchronisation avec les systèmes extérieurs à React (DOM, API, subscriptions, timers). Si vous pouvez calculer une valeur directement à partir des props/state actuels, n’utilisez pas d’effect.
Trois signes que vous utilisez mal :
- Vous utilisez un effect pour calculer une valeur dérivée à partir d’un autre state (utilisez une variable ordinaire ou
useMemo). - Vous utilisez un effect pour gérer un événement utilisateur (gérez directement dans le gestionnaire
onClick,onSubmit). - Vous utilisez un effect pour réinitialiser l’état quand une prop change — vous pouvez généralement utiliser la prop
keypour que React remonte automatiquement le composant au lieu d’utiliser un effect.
// Au lieu d'utiliser un effect pour réinitialiser le formulaire quand customerId change :
// useEffect(() => { setForm(initialForm); }, [customerId]);
// Utilisez key pour remonter le composant, React réinitialise automatiquement tous les états internes :
<CustomerForm key={customerId} customerId={customerId} />
10. Écrire des tests pour le comportement des hooks
Utilisez Jest avec React Testing Library pour tester le comportement réel plutôt que les détails d’implémentation.
npm install --save-dev @testing-library/react @testing-library/user-event jest
// CustomerCounter.test.jsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { CustomerCounter } from "./CustomerCounter";
test("augmente count au clic du bouton Ajouter un client", async () => {
const user = userEvent.setup();
render(<CustomerCounter />);
expect(screen.getByText("Nombre de clients : 0")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Ajouter un client" }));
expect(screen.getByText("Nombre de clients : 1")).toBeInTheDocument();
});
- Le test appelle
renderpuis simule un vrai clic viauserEvent, comme le comportement d’un utilisateur. - L’assertion repose sur le contenu affiché (
getByText), pas sur l’état interne — c’est la bonne façon de tester avec React : testez le comportement, pas l’implémentation.
PASS ./CustomerCounter.test.jsx
✓ augmente count au clic du bouton Ajouter un client (45 ms)
11. Conclusion
useState semble simple en apparence mais peut facilement causer des erreurs si vous oubliez que la valeur dans la closure est « gelée » à chaque rendu — utilisez la mise à jour fonctionnelle quand le nouvel état dépend de l’ancien. useEffect est puissant mais ne doit être utilisé que pour synchroniser avec les systèmes externes à React ; si une valeur peut être calculée directement lors du rendu, ne l’enrobez pas dans un effect. Maîtriser le tableau de dépendances et la fonction de nettoyage vous aidera à éviter la plupart des bugs courants associés à ces deux hooks.