Menu

Mettre à jour tableaux et objets dans l'état React

Pour modifier un tableau ou un objet dans l'état React, construisez-en un nouveau et passez-le au setter. Apprenez à ajouter, retirer, remplacer, insérer et trier des éléments, à mettre à jour des objets imbriqués, et pourquoi muter l'état laisse l'écran inchangé.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Dans React, vous mettez à jour un tableau ou un objet de l'état en en créant un nouveau et en le passant au setter. Ne modifiez jamais la valeur existante sur place : utilisez le spread (...), map, filter ou slice pour construire une copie avec votre modification, puis appelez setItems(copy).

[...items, { ... }] est un nouveau tableau avec un élément de plus, et filter renvoie un nouveau tableau sans l'élément retiré. Les deux laissent l'ancien tableau intact.

Pourquoi la mutation ne déclenche pas de rendu

Quand vous appelez un setter, React compare la nouvelle valeur à l'ancienne avec Object.is. Pour les tableaux et les objets, cela compare les références, pas le contenu. Si vous modifiez un tableau avec push et repassez le même tableau, React voit la même référence, conclut que rien n'a changé et saute le rendu.

Cliquez plusieurs fois sur "push". La console montre le tableau qui grandit, mais aucune nouvelle ligne de rendu n'apparaît et le texte reste a. Cliquez ensuite sur "spread" : un rendu a lieu et tous les éléments ajoutés apparaissent d'un coup, car ils étaient dans le tableau depuis le début. La mutation ne lève pas d'erreur, elle laisse seulement l'écran périmé jusqu'à ce que quelque chose d'autre déclenche un rendu. Cliquez encore une fois sur "push" après cela et cette fois un rendu peut avoir lieu : React ne peut pas toujours sauter le travail à l'avance, donc une mutation apparaît parfois et parfois non. C'est cette imprévisibilité qui doit vous dissuader de muter l'état.

Tableaux : méthodes à utiliser et à éviter

La règle est simple : utilisez les méthodes qui renvoient un nouveau tableau, évitez celles qui modifient le tableau sur lequel elles sont appelées.

ObjectifÀ éviter (mute)À utiliser (renvoie un nouveau tableau)
Ajouterpush, unshift[...arr, item], [item, ...arr]
Retirerpop, shift, splicefilter, slice
Remplacerarr[i] = x, splicemap, with(i, x)
Insérersplice[...arr.slice(0, i), x, ...arr.slice(i)], toSpliced
Trier, inversersort, reversetoSorted, toReversed, ou copier d'abord

toSorted, toReversed, toSpliced et with sont des méthodes de tableau plus récentes (ES2023), prises en charge par tous les grands navigateurs actuels depuis 2023. Elles font le même travail que leurs équivalents mutants, sur une copie.

Mettre à jour un élément avec map

Pour modifier un élément, parcourez le tableau avec map et renvoyez un nouvel objet pour l'élément voulu, et l'objet d'origine pour tous les autres.

La partie { ...todo, done: !todo.done } compte. Un bug courant consiste à copier le tableau mais à modifier l'objet à l'intérieur :

const next = [...todos];
next[0].done = true; // next is new, but next[0] is the same object as todos[0]
setTodos(next);

Cela déclenche un rendu, donc tout semble correct, mais l'ancien et le nouveau tableau partagent maintenant un objet modifié. Tout ce qui compare l'ancien élément au nouveau (un enfant React.memo, un effet qui dépend de l'élément) ne voit aucune différence. Copiez l'élément que vous modifiez.

Insérer à un index

Il n'existe pas d'insert non mutant, alors coupez le tableau en deux et placez le nouvel élément entre les moitiés. toSpliced(index, 0, item) fait la même chose en un seul appel.

Réglez l'index à 0 et insérez à nouveau : le nouveau C va en tête. Remplacez l'expression next par letters.toSpliced(index, 0, 'C') et le résultat est le même.

Trier et inverser

sort et reverse modifient le tableau sur lequel elles sont appelées, même si elles le renvoient aussi. Cela fait de setItems(items.sort()) une mutation suivie d'un set avec la même référence, c'est-à-dire le bug du début de cette page. Triez plutôt une copie.

[...scores].reverse() fonctionne parce que le spread crée d'abord la copie et que reverse ne modifie ensuite que cette copie.

Mettre à jour des objets et des objets imbriqués

Les objets suivent la même règle. Décomposez l'ancien objet dans un nouveau et remplacez les champs qui changent. Pour un objet imbriqué, décomposez à chaque niveau jusqu'au champ : une seule décomposition ne copie que le premier niveau, l'objet intérieur serait donc toujours l'ancien.

Chaque niveau copié est un nouvel objet ; chaque niveau que vous ne touchez pas reste le même objet. C'est ce qui rend le spread peu coûteux : si l'état contenait aussi une longue liste de commandes, { ...user } copierait la référence à la liste, pas la liste elle-même.

Si un objet de l'état est imbriqué sur trois ou quatre niveaux et que vous écrivez sans cesse de longues chaînes de spreads, c'est souvent le signe que l'état pourrait être plus plat. Stocker les éléments dans une table indexée par id, ou découper un gros objet en quelques appels à useState, garde chaque mise à jour courte. Quand de nombreux événements modifient une même structure, un reducer aide aussi (voir useReducer).

structuredClone ou spread

structuredClone(user) fait une copie profonde en un seul appel, vous pouvez donc ensuite modifier la copie librement :

const next = structuredClone(user);
next.address.city = 'Oslo';
setUser(next);

C'est correct, mais cela copie tout l'objet à chaque fois, y compris les parties que vous n'avez pas modifiées. Cela coûte du temps sur un gros état, et chaque objet imbriqué reçoit une nouvelle référence : un enfant mémoïsé qui reçoit user.orders refait donc un rendu même si les commandes n'ont pas changé. structuredClone lève aussi une erreur sur les fonctions, et les instances de classes reviennent sous forme d'objets simples. Le spread ne copie que le chemin modifié, c'est donc le choix habituel ; gardez structuredClone pour la rare mise à jour qui réécrit vraiment la majeure partie de l'objet.

Écrire des mutations avec Immer

Immer est une petite bibliothèque qui permet d'écrire du code qui ressemble à de la mutation. Elle vous donne un brouillon, enregistre ce que vous modifiez et produit une nouvelle valeur immuable dans laquelle seules les parties modifiées sont copiées. Le paquet use-immer l'enveloppe dans un hook :

import { useImmer } from 'use-immer';

function Profile() {
    const [user, updateUser] = useImmer({
        name: 'Ada',
        address: { city: 'London' },
    });

    function moveTo(city) {
        updateUser((draft) => {
            draft.address.city = city;
        });
    }
    // ...
}

Immer n'est pas disponible dans les éditeurs de cette page, qui ne chargent que React. Il vaut la peine de l'ajouter quand votre état est profondément imbriqué et que les spreads deviennent difficiles à lire. Pour un état plat, le spread et map suffisent, et c'est ce que vous verrez dans la plupart du code React.

Questions fréquentes

Comment ajouter un élément à un tableau dans l'état React ?

Créez un nouveau tableau qui contient les anciens éléments plus le nouveau et passez-le au setter : setItems([...items, newItem]). N'appelez pas items.push(newItem), car cela modifie le tableau existant et React voit la même référence.

Pourquoi mon composant ne refait-il pas de rendu quand je mets à jour un tableau ?

Vous avez très probablement modifié le tableau sur place (push, splice, sort ou arr[i] = x) et repassé le même tableau. React compare l'ancienne et la nouvelle valeur avec Object.is, trouve le même objet et saute le rendu. Passez plutôt un nouveau tableau.

Comment mettre à jour un objet dans un tableau en React ?

Utilisez map et renvoyez un nouvel objet pour l'élément qui change, et l'élément d'origine pour tous les autres : setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t)).

Comment mettre à jour un objet imbriqué dans l'état React ?

Copiez chaque niveau sur le chemin du champ que vous modifiez : setUser({ ...user, address: { ...user.address, city: 'Oslo' } }). Une seule décomposition ne copie que le premier niveau, l'objet intérieur serait donc toujours partagé.

Faut-il utiliser structuredClone ou l'opérateur spread pour l'état React ?

Préférez le spread. Il ne copie que ce qui change et garde les parties intactes comme les mêmes objets, ce qui aide React.memo et les comparaisons. structuredClone copie tout à chaque fois : c'est plus simple à écrire, mais plus lent sur un gros état, et cela casse l'égalité de référence des parties qui n'ont pas changé.

Illustration des langages de programmation de Coddy

Apprendre à coder avec Coddy

COMMENCER