
Gestion d'objets et de tableaux dans l'état (Immutabilité)
Apprenez pourquoi et comment gérer l'état des objets et tableaux en React en respectant l'immutabilité. Utilisez useState avec spread syntax, map, filter.
Le piège de la mutation directe
Lorsque vous stockez des objets ou des tableaux dans l'état d'un composant React à l'aide de `useState`, une erreur courante consiste à essayer de les modifier directement (mutation) avant d'appeler la fonction de mise à jour. Par exemple, modifier une propriété d'un objet ou utiliser la méthode `push` sur un tableau.
const [user, setUser] = useState({ name: 'Bob', age: 25 });
const [list, setList] = useState(['a', 'b']);
function handleAgeIncrease() {
// !!! MAUVAIS : Mutation directe de l'objet en état
user.age = user.age + 1;
setUser(user); // React risque de ne pas détecter le changement !
}
function handleAddItem() {
// !!! MAUVAIS : Mutation directe du tableau en état
list.push('c');
setList(list); // React risque de ne pas détecter le changement !
}
Pourquoi est-ce problématique ? React détermine s'il doit effectuer un nouveau rendu d'un composant après une mise à jour d'état en comparant l'ancienne référence de l'état avec la nouvelle. Lorsque vous mutez directement un objet ou un tableau, vous modifiez son contenu interne, mais la référence (l'adresse en mémoire) de l'objet ou du tableau lui-même ne change pas. Par conséquent, lorsque React compare l'ancienne référence `user` avec la nouvelle (qui est la même référence après `setUser(user)`), il conclut qu'aucun changement n'a eu lieu et ne déclenche pas le nouveau rendu nécessaire pour afficher la modification dans l'interface utilisateur.
Le principe d'immutabilité : Créer plutôt que modifier
La solution à ce problème réside dans le respect du principe d'immutabilité. Au lieu de modifier l'objet ou le tableau existant, vous devez créer une nouvelle instance de l'objet ou du tableau qui contient les modifications souhaitées. Cette nouvelle instance aura une référence différente de l'ancienne, permettant ainsi à React de détecter le changement et de déclencher le re-rendu.
L'immutabilité est un concept clé non seulement en React mais aussi dans de nombreux paradigmes de programmation (notamment fonctionnelle). Elle rend les changements d'état plus explicites, prévisibles et facilite le débogage ainsi que certaines optimisations de performance.
Mettre à jour des objets de manière immuable
Pour mettre à jour un objet dans l'état sans le muter, la technique la plus courante est d'utiliser l'opérateur de décomposition (spread syntax `...`). Cet opérateur permet de copier toutes les paires clé-valeur d'un objet existant dans un nouvel objet. Vous pouvez ensuite spécifier les propriétés que vous souhaitez modifier ou ajouter, qui écraseront ou compléteront les propriétés copiées.
const [user, setUser] = useState({ id: 1, name: 'Alice', city: 'Paris' });
// Mettre à jour la ville
function updateCity(newCity) {
setUser(currentUser => ({ // Utilisation de la mise à jour fonctionnelle recommandée
...currentUser, // 1. Copie toutes les propriétés de l'objet actuel
city: newCity // 2. Ecrase la propriété 'city' avec la nouvelle valeur
}));
}
// Ajouter une nouvelle propriété
function addJob(jobTitle) {
setUser(currentUser => ({
...currentUser,
job: jobTitle // Ajoute la nouvelle clé 'job'
}));
}
Dans cet exemple, `setUser` reçoit un tout nouvel objet à chaque appel. La référence change, et React détecte la mise à jour.
Pour les objets imbriqués, vous devez appliquer ce principe récursivement pour chaque niveau que vous modifiez :
const [data, setData] = useState({ user: { name: 'Alice', address: { street: '123 Main St', city: 'Paris' } }, status: 'active' });
function updateStreet(newStreet) {
setData(currentData => ({
...currentData, // Copie le niveau supérieur ('status')
user: { // Crée un nouvel objet 'user'
...currentData.user, // Copie les propriétés de 'user' ('name')
address: { // Crée un nouvel objet 'address'
...currentData.user.address, // Copie les propriétés de 'address' ('city')
street: newStreet // Met à jour 'street'
}
}
}));
}
Mettre à jour des tableaux de manière immuable
Pour les tableaux, le principe est le même : ne pas utiliser de méthodes qui modifient le tableau sur place (comme `push`, `pop`, `splice`, `sort` directement sur la variable d'état), mais plutôt des méthodes ou des techniques qui retournent un nouveau tableau.
- Ajouter un élément : Utilisez la syntaxe de décomposition (`...`) ou `concat`.
const [items, setItems] = useState(['apple', 'banana']);
function addItem(newItem) {
// Avec spread syntax (préféré car plus concis)
setItems(currentItems => [...currentItems, newItem]); // Ajoute à la fin
// setItems(currentItems => [newItem, ...currentItems]); // Ajoute au début
// Avec concat (retourne un nouveau tableau)
// setItems(currentItems => currentItems.concat(newItem));
}
- Supprimer un élément : Utilisez la méthode `filter`, qui retourne un nouveau tableau contenant uniquement les éléments qui satisfont une condition.
function removeItem(itemToRemove) {
setItems(currentItems => currentItems.filter(item => item !== itemToRemove));
}
// Supprimer par index (en utilisant slice ou filter avec index)
function removeItemByIndex(indexToRemove) {
setItems(currentItems => currentItems.filter((_, index) => index !== indexToRemove));
// Alternative avec slice:
// setItems(currentItems => [
// ...currentItems.slice(0, indexToRemove),
// ...currentItems.slice(indexToRemove + 1)
// ]);
}
- Modifier un élément : Utilisez la méthode `map`, qui retourne un nouveau tableau où chaque élément est le résultat d'une fonction appliquée à l'élément d'origine.
const [tasks, setTasks] = useState([
{ id: 1, text: 'Faire les courses', completed: false },
{ id: 2, text: 'Apprendre React', completed: true },
]);
// Marquer une tâche comme complétée/non complétée
function toggleTaskCompletion(taskId) {
setTasks(currentTasks =>
currentTasks.map(task => {
if (task.id === taskId) {
// Trouvé ! Retourne un *nouvel objet* tâche avec 'completed' inversé
return { ...task, completed: !task.completed };
}
// Pas la tâche cible, retourne l'objet tâche original (pas de mutation)
return task;
})
);
}
Pourquoi l'immutabilité est essentielle en React ?
Adopter l'immutabilité pour la gestion de l'état en React n'est pas juste une convention, c'est une pratique fondamentale qui apporte plusieurs avantages :
- Détection fiable des changements : Comme mentionné, React se base sur la comparaison de références pour savoir quand re-rendre. L'immutabilité garantit que les changements créent de nouvelles références, rendant la détection de changement simple et efficace.
- Optimisation des performances : Savoir que les objets/tableaux ne sont jamais mutés permet à React d'implémenter des optimisations. Par exemple, `React.memo` peut éviter un re-rendu si les props (qui peuvent être des objets ou tableaux) n'ont pas changé de référence.
- Prévisibilité et débogage : Les changements d'état deviennent plus faciles à suivre. Des outils comme les Redux DevTools peuvent enregistrer chaque état successif car chaque état est une entité distincte, permettant le "time-travel debugging".
- Préparation pour le futur : Les fonctionnalités avancées de React, comme le rendu concurrent (Concurrent Mode), reposent fortement sur l'immutabilité pour fonctionner correctement sans effets de bord inattendus.
En résumé, même si cela demande un léger changement d'habitude par rapport à la mutation directe, traiter l'état (objets et tableaux) de manière immuable est crucial pour écrire des applications React robustes, performantes et maintenables.