
`useState` - Revisité et cas avancés
Maîtrisez les aspects avancés du hook useState de React : initialisation paresseuse, mises à jour fonctionnelles et gestion immuable des objets/tableaux.
Au-delà des bases de `useState`
Le hook `useState` est souvent le premier que l'on rencontre en React. Il permet d'ajouter une variable d'état locale à un composant fonctionnel. Si son utilisation de base pour des types primitifs (chaînes, nombres, booléens) est simple, sa pleine puissance se révèle dans des scénarios plus complexes. Cette section explore des techniques avancées pour optimiser son usage, garantir la cohérence de l'état et gérer efficacement des structures de données plus élaborées.
Nous allons revisiter `useState` sous un angle plus approfondi, en nous concentrant sur trois aspects clés : l'initialisation paresseuse pour optimiser les performances initiales, les mises à jour fonctionnelles pour gérer les dépendances à l'état précédent de manière fiable, et les principes d'immutabilité essentiels lors de la manipulation d'objets et de tableaux dans l'état. Maîtriser ces concepts vous permettra d'écrire des composants plus robustes et performants.
Initialisation paresseuse : Optimiser le premier rendu
L'argument passé à `useState` détermine la valeur initiale de l'état. Si ce calcul initial est coûteux (par exemple, lire depuis `localStorage` ou effectuer une opération complexe), il sera réexécuté à chaque rendu du composant, même si la valeur initiale n'est utilisée qu'une seule fois. Pour éviter ce travail inutile, React propose l'initialisation paresseuse.
L'initialisation paresseuse consiste à passer une fonction à `useState` au lieu d'une valeur directe. React n'appellera cette fonction que lors du premier rendu pour obtenir la valeur initiale. Les rendus suivants ignoreront cette fonction.
Prenons l'exemple d'une valeur initiale calculée par une fonction potentiellement lourde :
function calculateInitialValue() {
// Simulation d'un calcul coûteux
console.log("Calcul initial...");
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += i;
}
return sum;
}
function MonComposant() {
// Mauvais : calculateInitialValue() est appelée à chaque rendu
// const [valeur, setValeur] = useState(calculateInitialValue());
// Bon : Utilisation de l'initialisation paresseuse
const [valeur, setValeur] = useState(() => calculateInitialValue());
// ... reste du composant
return Valeur: {valeur};
}
Avec l'initialisation paresseuse (deuxième `useState`), le message "Calcul initial..." n'apparaîtra qu'une seule fois dans la console, lors du montage initial du composant, améliorant ainsi les performances si le calcul est réellement coûteux.
Mises à jour fonctionnelles : Garantir la fraîcheur de l'état
Lorsque la nouvelle valeur de l'état dépend de sa valeur précédente, il est crucial d'utiliser la forme fonctionnelle de la fonction de mise à jour fournie par `useState`. En effet, les mises à jour d'état dans React peuvent être asynchrones et regroupées (batching) pour des raisons de performance.
Si vous vous basez sur la valeur actuelle de l'état directement dans l'appel de mise à jour, vous risquez d'utiliser une valeur obsolète (stale state), surtout si plusieurs mises à jour sont déclenchées rapidement (par exemple, dans un gestionnaire d'événement).
La forme fonctionnelle reçoit l'état précédent (le plus récent garanti par React) comme argument et retourne le nouvel état. Cela assure que votre mise à jour se base toujours sur la dernière valeur connue par React.
import React, { useState } from 'react';
function Compteur() {
const [count, setCount] = useState(0);
const incrementTroisFois = () => {
// Mauvais : Peut ne pas incrémenter de 3 si les mises à jour sont groupées
// setCount(count + 1);
// setCount(count + 1);
// setCount(count + 1);
// Bon : Utilisation des mises à jour fonctionnelles
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
};
return (
Compte : {count}
);
}
export default Compteur;
Dans cet exemple, la version commentée pourrait ne pas incrémenter le compteur de 3 à chaque clic, car `count` pourrait avoir la même valeur (obsolète) lors des trois appels. La version utilisant la fonction `prevCount => prevCount + 1` garantit que chaque incrémentation se base sur la valeur précédente correcte, résultant bien en une augmentation de 3.
Gestion des objets et tableaux : Le principe d'immutabilité
Un principe fondamental en React (et en programmation fonctionnelle) est l'immutabilité. Cela signifie qu'au lieu de modifier directement un objet ou un tableau existant dans l'état, vous devez toujours créer une nouvelle version de cet objet ou tableau avec les modifications souhaitées.
React utilise une comparaison de référence (shallow comparison) pour détecter si l'état a changé et s'il faut déclencher un nouveau rendu. Si vous mutez directement un objet ou un tableau, sa référence en mémoire ne change pas. React ne détectera donc pas la modification et ne mettra pas à jour l'interface utilisateur, même si les données internes ont changé.
Pour mettre à jour un objet dans l'état, utilisez l'opérateur de décomposition (spread syntax `...`) pour copier les anciennes propriétés et spécifier les nouvelles valeurs :
const [user, setUser] = useState({ name: 'Alice', age: 30 });
const handleAgeIncrement = () => {
// Mauvais : Mutation directe de l'objet
// user.age = user.age + 1;
// setUser(user); // React ne verra pas de changement de référence
// Bon : Création d'un nouvel objet
setUser(prevUser => ({
...prevUser, // Copie toutes les propriétés existantes
age: prevUser.age + 1 // Ecrase la propriété 'age'
}));
};
Pour les tableaux, utilisez également la décomposition ou des méthodes de tableau qui retournent un nouveau tableau (`map`, `filter`, `concat`, `slice`, etc.) au lieu de méthodes qui modifient le tableau sur place (`push`, `splice`, `sort`).
const [items, setItems] = useState(['pomme', 'banane']);
const addItem = (newItem) => {
// Mauvais : Mutation directe
// items.push(newItem);
// setItems(items);
// Bon : Création d'un nouveau tableau
setItems(prevItems => [...prevItems, newItem]); // Ajout à la fin
};
const removeItem = (itemToRemove) => {
// Bon : Utilisation de filter pour créer un nouveau tableau
setItems(prevItems => prevItems.filter(item => item !== itemToRemove));
};
const updateItem = (oldItem, newItem) => {
// Bon : Utilisation de map pour créer un nouveau tableau
setItems(prevItems => prevItems.map(item => (item === oldItem ? newItem : item)));
}
Respecter l'immutabilité est essentiel pour assurer la prévisibilité des mises à jour d'état, faciliter le débogage et permettre à React d'optimiser les rendus efficacement.
Points clés à retenir sur useState avancé
En résumé, pour une utilisation optimale de `useState` dans des cas non triviaux :
- Utilisez l'initialisation paresseuse (passer une fonction à `useState`) lorsque le calcul de la valeur initiale est coûteux, afin de ne l'exécuter qu'une seule fois.
- Privilégiez les mises à jour fonctionnelles (`setState(prevState => ...)`) lorsque le nouvel état dépend de l'état précédent, pour éviter les problèmes liés à l'état obsolète (stale state) dû au batching asynchrone.
- Respectez scrupuleusement l'immutabilité lors de la mise à jour d'objets ou de tableaux : créez toujours de nouvelles instances au lieu de modifier les existantes, en utilisant la syntaxe de décomposition (`...`) ou des méthodes de tableau appropriées.
L'application rigoureuse de ces techniques vous aidera à écrire des composants React plus performants, plus fiables et plus faciles à maintenir, surtout lorsque la complexité de la gestion d'état augmente.