🧠 State et cycle de vie des composants (explications approfondies)
Le state est la donnée locale propre à un composant React. Quand le state change, React re-renderise le composant (et ses enfants si nécessaire) pour mettre à jour l’interface.
Cette page explique :
- comment utiliser
useStateetuseReducerpour gérer l’état ; - comment manipuler des objets et tableaux d’état de façon immuable ;
- comment
useEffectremplace les méthodes de cycle de vie des classes ; - bonnes pratiques et pièges courants (batching, valeurs asynchrones, initialisation paresseuse).
Exemple basique avec useState
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0); // état initial = 0
return (
<div>
<p>Vous avez cliqué {count} fois</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}Explications :
useState(0)crée un étatcountinitialisé à0et retourne[count, setCount].setCountremplace la valeur decount(pas de fusion automatique comme pourthis.setStateen classes). Pour modifier en fonction de la valeur précédente, utilisez la forme fonctionnelle (voir ci-dessous).
Mise à jour basée sur l’état précédent (forme fonctionnelle)
Lorsque la nouvelle valeur dépend de l’ancienne, préférez :
setCount(prev => prev + 1);Pourquoi ?
- React peut regrouper (batch) les mises à jour d’état. Si vous écrivez
setCount(count + 1)plusieurs fois dans la même phase, toutes les lectures decountpeuvent correspondre à l’ancienne valeur et produire un résultat inattendu. En passant une fonction, React fournit la valeur la plus récente (prev).
Initialisation paresseuse
Si l’état initial est coûteux à calculer, vous pouvez passer une fonction à useState :
const [data, setData] = useState(() => expensiveInit());La fonction expensiveInit ne sera appelée qu’une seule fois au montage.
Travailler avec des objets et tableaux (immutabilité)
Ne muter jamais directement l’objet/array d’état. Exemple : ajouter un élément à un tableau :
const [items, setItems] = useState([]);
function add(item) {
// Correct : créer un nouveau tableau
setItems(prev => [...prev, item]);
}
function remove(index) {
setItems(prev => prev.filter((_, i) => i !== index));
}Pour des objets :
const [user, setUser] = useState({ name: 'Alice', age: 25 });
// Mettre à jour une propriété sans écraser les autres
setUser(prev => ({ ...prev, age: prev.age + 1 }));useReducer pour des états complexes
useReducer est utile quand la logique d’état devient complexe ou quand plusieurs sous-valeurs sont mises à jour ensemble.
import { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'decrement':
return { ...state, count: state.count - 1 };
case 'reset':
return action.payload;
default:
throw new Error('Action inconnue');
}
}
export function CounterReducer() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Compteur : {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}Cycle de vie : useEffect vs méthodes de classe
Dans les composants de classe, on utilisait componentDidMount, componentDidUpdate et componentWillUnmount.
Avec les Hooks, useEffect couvre ces cas selon la dépendance fournie :
import { useEffect } from 'react';
// équivalent de componentDidMount
useEffect(() => {
// code de montage
}, []); // tableau vide = exécute une seule fois au montage
// équivalent de componentDidUpdate (dépendances)
useEffect(() => {
// code exécuté quand `value` change
}, [value]);
// nettoyage / componentWillUnmount
useEffect(() => {
const id = setInterval(() => {}, 1000);
return () => clearInterval(id); // exécuté au démontage
}, []);Points clés sur useEffect :
- L’effet s’exécute après le rendu (commit) ; n’y mettez pas de code bloquant.
- Le tableau des dépendances indique quand ré-exécuter l’effet.
- Toujours déclarer toutes les dépendances nécessaires (ou documenter pourquoi vous les omettez).
Batching et nature asynchrone des mises à jour
Les appels à setState (useState/setWhatever) ne mettent pas à jour la variable locale immédiatement dans le même rendu. React planifie la mise à jour et re-renderise le composant. Depuis React 18, les mises à jour sont automatiquement groupées (batched) même en dehors des gestionnaires d’événements.
Exemple problématique :
setCount(count + 1);
setCount(count + 1);
// peut n'augmenter que de 1 si React batch les mises à jourUtilisez la forme fonctionnelle pour garantir le comportement attendu.
Exemple complet commenté
import { useState, useEffect } from 'react';
export default function TodoApp() {
const [todos, setTodos] = useState(() => {
// initialisation paresseuse depuis le localStorage
const raw = localStorage.getItem('todos');
return raw ? JSON.parse(raw) : [];
});
const [input, setInput] = useState('');
// sauvegarder dans localStorage quand todos change
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
function addTodo() {
if (!input.trim()) return;
setTodos(prev => [...prev, { id: Date.now(), text: input }]);
setInput('');
}
function removeTodo(id) {
setTodos(prev => prev.filter(t => t.id !== id));
}
return (
<div>
<h3>Todo</h3>
<input value={input} onChange={e => setInput(e.target.value)} />
<button onClick={addTodo}>Ajouter</button>
<ul>
{todos.map(t => (
<li key={t.id}>
{t.text} <button onClick={() => removeTodo(t.id)}>Supprimer</button>
</li>
))}
</ul>
</div>
);
}Commentaires sur l’exemple :
- Initialisation paresseuse : on lit
localStorageune seule fois au montage. useEffectavec[todos]sauvegarde automatiquement l’état quand il change.- Les fonctions
addTodo/removeTodoutilisent la forme fonctionnelle desetTodospour éviter les problèmes de concurrence.
Quand préférer useReducer ?
- Lorsque l’état contient plusieurs sous-valeurs liées (par ex. formulaire complexe).
- Quand vous voulez centraliser la logique d’état (actions, reducers), ce qui facilite le test.
Bonnes pratiques rapides
- Privilégiez les composants fonctionnels + Hooks pour les nouveaux développements.
- Gardez l’état local aussi petit que possible : dérivez ce que vous pouvez.
- Ne muter jamais directement l’état ; renvoyez de nouveaux objets/arrays.
- Documentez pourquoi vous omettez une dépendance dans
useEffectsi nécessaire.
📝 TL;DR
useStatepour l’état local simple.useReducerpour les états complexes ou les transitions explicitement nommées.useEffectremplace la plupart des méthodes de cycle de vie des classes (montage, mises à jour, nettoyage).- Utilisez la forme fonctionnelle
setState(prev => ...)quand la nouvelle valeur dépend de l’ancienne. - Initialisation paresseuse avec
useState(() => ...)pour des coûts d’initialisation.