Mettre à jour les tableaux d’un état

Les tableaux sont un type d’objet modifiable en JavaScript que vous pouvez stocker dans un état et que vous devez traiter comme étant en lecture seule. Tout comme avec les objets, lorsque vous souhaitez mettre à jour un tableau stocké dans un état, vous devez en créer un nouveau (ou en copier un existant), puis affecter le nouveau tableau dans l’état.

Vous allez apprendre

  • Comment ajouter, supprimer ou modifier des éléments dans un tableau dans l’état React
  • Comment mettre à jour un objet à l’intérieur d’un tableau
  • Comment rendre la copie de tableaux moins répétitive avec Immer

Mettre à jour des tableaux sans modification en place

En JavaScript, les tableaux sont des objets comme les autres. Tout comme avec les objets, vous devez considérer les tableaux dans l’état React comme étant en lecture seule. Ça signifie que vous ne devez pas réaffecter les éléments à l’intérieur d’un tableau, comme dans arr[0] = 'oiseau', et vous ne devez pas non plus utiliser des méthodes qui modifient le tableau en place, telles que push() et pop().

Au lieu de ça, chaque fois que vous souhaitez mettre à jour un tableau, vous devez passer un nouveau tableau à la fonction de mise à jour de l’état. Pour cela, vous pouvez créer un nouveau tableau à partir de l’original en utilisant des méthodes non modifiantes telles que filter() et map(). Ensuite, vous pouvez mettre à jour l’état avec le nouveau tableau résultant.

Voici un tableau de référence des opérations courantes sur les tableaux. Lorsque vous traitez des tableaux dans l’état de React, évitez les méthodes de la colonne de gauche et privilégiez celles de la colonne de droite :

à éviter (modifie le tableau)à privilégier (renvoie un nouveau tableau)
ajoutpush, unshiftconcat, syntaxe de spread [...arr] (exemple)
suppressionpop, shift, splicefilter, slice (exemple)
remplacementsplice, affectation arr[i] = ...map (exemple)
trireverse, sortcopiez d’abord le tableau (exemple)

Vous pouvez également utiliser Immer qui vous permet d’utiliser des méthodes des deux colonnes.

Piège

Malheureusement, slice et splice ont des noms similaires mais sont très différents :

  • slice vous permet de copier un tableau ou une partie de celui-ci.
  • splice modifie le tableau (pour insérer ou supprimer des éléments).

En React, vous utiliserez beaucoup plus souvent slice (sans le p !) car vous ne voulez pas modifier en place les objets ou les tableaux dans l’état. La page Mettre à jour les objets d’un état explique ce qu’est la modification en place, et pourquoi elle est déconseillée pour l’état.

Ajouter un élément à un tableau

push() modifiera un tableau, ce que vous ne souhaitez pas faire :

import { useState } from 'react';

let nextId = 0;

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState([]);

  return (
    <>
      <h1>Sculpteurs inspirants :</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={() => {
        artists.push({
          id: nextId++,
          name: name,
        });
      }}>Ajouter</button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

Au lieu de ça, créez un nouveau tableau qui contient les éléments existants et un nouvel élément à la fin. Il existe plusieurs façons de le faire, mais la plus simple consiste à utiliser la syntaxe de spread de tableaux ... :

setArtists( // Remplace l'état
[ // par un nouveau tableau
...artists, // qui contient tous les anciens éléments
{ id: nextId++, name: name } // et un nouvel élément à la fin
]
);

Maintenant, ça fonctionne correctement :

import { useState } from 'react';

let nextId = 0;

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState([]);

  return (
    <>
      <h1>Sculpteurs inspirants :</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={() => {
        setArtists([
          ...artists,
          { id: nextId++, name: name }
        ]);
      }}>Ajouter</button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

La syntaxe de spread de tableaux permet également d’ajouter un élément au début du tableau en le plaçant avant le ...artists d’origine :

setArtists([
{ id: nextId++, name: name },
...artists // Place les anciens éléments à la fin
]);

De cette manière, l’opérateur de spread peut à la fois agir comme push(), en ajoutant un élément à la fin d’un tableau, et comme unshift(), en ajoutant un élément au début d’un tableau. Essayez de l’utiliser dans le bac à sable ci-dessus !

Retirer un élément d’un tableau

Le moyen le plus simple de retirer un élément d’un tableau consiste à le filtrer. En d’autres termes, vous allez créer un nouveau tableau qui ne contiendra pas cet élément. Pour cela, utilisez la méthode filter des tableaux, par exemple:

import { useState } from 'react';

let initialArtists = [
  { id: 0, name: 'Marta Colvin Andrade' },
  { id: 1, name: 'Lamidi Olonade Fakeye'},
  { id: 2, name: 'Louise Nevelson'},
];

export default function List() {
  const [artists, setArtists] = useState(
    initialArtists
  );

  return (
    <>
      <h1>Sculpteurs inspirants :</h1>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>
            {artist.name}{' '}
            <button onClick={() => {
              setArtists(
                artists.filter(a =>
                  a.id !== artist.id
                )
              );
            }}>
              Supprimer
            </button>
          </li>
        ))}
      </ul>
    </>
  );
}

Cliquez sur le bouton « Supprimer » plusieurs fois et observez son gestionnaire de clics.

setArtists(
artists.filter(a => a.id !== artist.id)
);

Ici, artists.filter(a => a.id !== artist.id) signifie « crée un tableau comprenant les artists dont les IDs sont différents de artist.id ». En d’autres termes, le bouton « Supprimer » de chaque artiste filtre cet artiste du tableau, puis demande un nouveau rendu avec le tableau résultant. Notez que filter ne modifie pas le tableau d’origine.

Transformer un tableau

Si vous souhaitez modifier tout ou partie des éléments du tableau, vous pouvez utiliser map() pour créer un nouveau tableau. La fonction que vous passerez à map décidera quoi faire avec chaque élément en fonction de ses données ou de son index (ou les deux).

Dans cet exemple, un tableau contient les coordonnées de deux cercles et d’un carré. Lorsque vous appuyez sur le bouton, seuls les cercles sont déplacés de 50 pixels vers le bas. On y parvient en produisant un nouveau tableau de données à l’aide de map() :

import { useState } from 'react';

let initialShapes = [
  { id: 0, type: 'circle', x: 50, y: 100 },
  { id: 1, type: 'square', x: 150, y: 100 },
  { id: 2, type: 'circle', x: 250, y: 100 },
];

export default function ShapeEditor() {
  const [shapes, setShapes] = useState(
    initialShapes
  );

  function handleClick() {
    const nextShapes = shapes.map(shape => {
      if (shape.type === 'square') {
        // Pas de changement
        return shape;
      } else {
        // Renvoie un nouveau cercle décalé de 50px vers le bas
        return {
          ...shape,
          y: shape.y + 50,
        };
      }
    });
    // Nouveau rendu avec le nouveau tableau
    setShapes(nextShapes);
  }

  return (
    <>
      <button onClick={handleClick}>
        Déplacez les cercles vers le bas !
      </button>
      {shapes.map(shape => (
        <div
          key={shape.id}
          style={{
          background: 'purple',
          position: 'absolute',
          left: shape.x,
          top: shape.y,
          borderRadius:
            shape.type === 'circle'
              ? '50%' : '',
          width: 20,
          height: 20,
        }} />
      ))}
    </>
  );
}

Remplacer des éléments dans un tableau

Il est très courant de vouloir remplacer un ou plusieurs éléments dans un tableau. Les affectations telles que arr[0] = 'oiseau' modifient le tableau d’origine, vous devrez donc encore une fois plutôt utiliser map.

Pour remplacer un élément, créez un nouveau tableau avec map. À l’intérieur de votre appel à map, vous recevrez l’index de l’élément comme deuxième argument. Utilisez-le pour décider s’il faut renvoyer l’élément d’origine (premier argument) ou autre chose :

import { useState } from 'react';

let initialCounters = [
  0, 0, 0
];

export default function CounterList() {
  const [counters, setCounters] = useState(
    initialCounters
  );

  function handleIncrementClick(index) {
    const nextCounters = counters.map((c, i) => {
      if (i === index) {
        // Incrémente le compteur cliqué
        return c + 1;
      } else {
        // Les autres n'ont pas changé
        return c;
      }
    });
    setCounters(nextCounters);
  }

  return (
    <ul>
      {counters.map((counter, i) => (
        <li key={i}>
          {counter}
          <button onClick={() => {
            handleIncrementClick(i);
          }}>+1</button>
        </li>
      ))}
    </ul>
  );
}

Insérer un élément dans un tableau

Parfois, vous souhaiterez peut-être insérer un élément à une position spécifique qui n’est ni au début ni à la fin du tableau. Pour cela, vous pouvez utiliser la syntaxe de spread de tableaux ... combinée avec la méthode slice(). La méthode slice() vous permet de découper une « tranche » du tableau. Pour insérer un élément, vous créerez un nouveau tableau qui contiendra la « tranche » avant le point d’insertion, puis le nouvel élément, et enfin le reste du tableau d’origine.

Dans cet exemple, le bouton Insérer insère toujours à l’index 1 :

import { useState } from 'react';

let nextId = 3;
const initialArtists = [
  { id: 0, name: 'Marta Colvin Andrade' },
  { id: 1, name: 'Lamidi Olonade Fakeye'},
  { id: 2, name: 'Louise Nevelson'},
];

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState(
    initialArtists
  );

  function handleClick() {
    const insertAt = 1; // Peut être n’importe quel index
    const nextArtists = [
      // Éléments avant le point d’insertion :
      ...artists.slice(0, insertAt),
      // Nouvel élément :
      { id: nextId++, name: name },
      // Éléments après le point d’insertion :
      ...artists.slice(insertAt)
    ];
    setArtists(nextArtists);
    setName('');
  }

  return (
    <>
      <h1>Sculpteurs inspirants :</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={handleClick}>
        Insérer
      </button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

Apporter d’autres modifications à un tableau

Il y a certaines choses que vous ne pouvez pas faire en utilisant seulement la syntaxe de spread et des méthodes non modifiantes telles que map() et filter(). Par exemple, vous pourriez vouloir inverser ou trier un tableau. Les méthodes reverse() et sort() de JavaScript modifient le tableau d’origine, vous ne pouvez donc pas les utiliser directement.

Cependant, vous pouvez d’abord copier le tableau, puis apporter des modifications à cette copie.

Par exemple :

import { useState } from 'react';

const initialList = [
  { id: 0, title: 'Big Bellies' },
  { id: 1, title: 'Lunar Landscape' },
  { id: 2, title: 'Terracotta Army' },
];

export default function List() {
  const [list, setList] = useState(initialList);

  function handleClick() {
    const nextList = [...list];
    nextList.reverse();
    setList(nextList);
  }

  return (
    <>
      <button onClick={handleClick}>
        Inverser
      </button>
      <ul>
        {list.map(artwork => (
          <li key={artwork.id}>{artwork.title}</li>
        ))}
      </ul>
    </>
  );
}

Ici, vous utilisez d’abord la syntaxe de spread [...list] pour créer une copie du tableau d’origine. Maintenant que vous avez une copie, vous pouvez utiliser des méthodes modifiantes comme nextList.reverse() ou nextList.sort(), ou même affecter individuellement des éléments avec nextList[0] = "quelque chose".

Cependant, même si vous copiez un tableau, vous ne pouvez pas modifier directement les éléments existants à l’intérieur de celui-ci. C’est parce que la copie est superficielle : le nouveau tableau contiendra les mêmes éléments que le tableau d’origine. Ainsi, si vous modifiez un objet à l’intérieur du tableau copié, vous modifiez l’état existant. Par exemple, le code suivant est problématique.

const nextList = [...list];
nextList[0].seen = true; // Problème : modifie list[0]
setList(nextList);

Bien que nextList et list soient deux tableaux différents, nextList[0] et list[0] pointent vers le même objet. Donc, en modifiant nextList[0].seen, vous modifiez également list[0].seen. C’est une mutation de l’état, que vous devez éviter ! Vous pouvez résoudre ce problème de la même manière que pour mettre à jour des objets JavaScript imbriqués en copiant les éléments individuels que vous souhaitez changer au lieu de les modifier. Voici comment faire.

Mettre à jour des objets dans des tableaux

Les objets ne sont pas vraiment « à l’intérieur » des tableaux. Ils peuvent sembler être « à l’intérieur » dans le code, mais chaque objet dans un tableau est une valeur distincte vers laquelle le tableau « pointe ». C’est pourquoi vous devez faire attention lorsque vous modifiez des champs imbriqués tels que list[0]. La liste d’œuvres d’art d’une autre personne peut pointer vers le même élément du tableau !

Lorsque vous mettez à jour un état imbriqué, vous devez créer des copies à partir de l’endroit où vous souhaitez effectuer la mise à jour, en remontant jusqu’au plus haut niveau. Voyons comment ça fonctionne.

Dans cet exemple, deux listes d’œuvres d’art séparées ont le même état initial. Elles sont censées être isolées, mais à cause d’une modification directe, leur état est accidentellement partagé, et cocher une case dans l’une des listes affecte l’autre liste :

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [myList, setMyList] = useState(initialList);
  const [yourList, setYourList] = useState(
    initialList
  );

  function handleToggleMyList(artworkId, nextSeen) {
    const myNextList = [...myList];
    const artwork = myNextList.find(
      a => a.id === artworkId
    );
    artwork.seen = nextSeen;
    setMyList(myNextList);
  }

  function handleToggleYourList(artworkId, nextSeen) {
    const yourNextList = [...yourList];
    const artwork = yourNextList.find(
      a => a.id === artworkId
    );
    artwork.seen = nextSeen;
    setYourList(yourNextList);
  }

  return (
    <>
      <h1>Liste d’œuvres d’art</h1>
      <h2>Ma liste à voir absolument :</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Votre liste à voir absolument :</h2>
      <ItemList
        artworks={yourList}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

Le problème se trouve dans du code comme celui-ci :

const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // Problème : modifie un élément existant
setMyList(myNextList);

Bien que le tableau myNextList soit nouveau, les éléments eux-mêmes sont les mêmes que dans le tableau myList d’origine. Donc, en changeant artwork.seen, vous modifiez l’œuvre d’art d’origine. Cette œuvre d’art est également dans yourList, ce qui provoque le bug. Des bugs comme celui-ci peuvent être difficiles à comprendre, mais heureusement, ils n’ont pas lieu si vous évitez de modifier l’état.

Vous pouvez utiliser map pour remplacer un ancien élément par sa nouvelle version sans mutation.

setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Crée un *nouvel* objet avec les modifications
return { ...artwork, seen: nextSeen };
} else {
// Pas de changement
return artwork;
}
}));

Ici, ... est la syntaxe de spread d’objets utilisée pour créer une copie d’un objet.

Avec cette approche, aucun des éléments de l’état existant n’est modifié et le bug est corrigé :

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [myList, setMyList] = useState(initialList);
  const [yourList, setYourList] = useState(
    initialList
  );

  function handleToggleMyList(artworkId, nextSeen) {
    setMyList(myList.map(artwork => {
      if (artwork.id === artworkId) {
        // Crée un *nouvel* objet avec les modifications
        return { ...artwork, seen: nextSeen };
      } else {
        // Pas de changement
        return artwork;
      }
    }));
  }

  function handleToggleYourList(artworkId, nextSeen) {
    setYourList(yourList.map(artwork => {
      if (artwork.id === artworkId) {
        // Crée un *nouvel* objet avec les modifications
        return { ...artwork, seen: nextSeen };
      } else {
        // Pas de changement
        return artwork;
      }
    }));
  }

  return (
    <>
      <h1>Liste d’œuvres d’art</h1>
      <h2>Ma liste à voir absolument :</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Votre liste à voir absolument :</h2>
      <ItemList
        artworks={yourList}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

En général, vous ne devriez modifier que les objets que vous venez de créer. Si vous insérez une nouvelle œuvre d’art, vous pouvez la modifier, mais si vous traitez quelque chose qui est déjà dans l’état, vous devez faire une copie.

Écrire une logique de mise à jour concise avec Immer

Mettre à jour des tableaux imbriqués sans modification directe peut conduire à du code un peu répétitif. Tout comme avec les objets :

  • En général, vous ne devriez pas avoir besoin de mettre à jour l’état à plus de quelques niveaux de profondeur. Si vos objets d’état sont très profonds, vous pouvez envisager de les restructurer différemment pour les rendre plus plats.
  • Si vous ne souhaitez pas changer la structure de votre état, vous préférerez peut-être utiliser Immer, qui vous permet d’écrire votre code en utilisant une syntaxe pratique mais modifiante, et se charge de produire les copies pour vous.

Voici l’exemple de la liste des œuvres d’art réécrit avec Immer :

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

Notez qu’avec Immer, une mutation comme artwork.seen = nextSeen est désormais autorisée :

updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});

C’est parce que vous ne modifiez pas l’état d’origine, mais un objet draft spécial fourni par Immer. De même, vous pouvez appliquer des méthodes modifiantes telles que push() et pop() au contenu du draft.

En interne, Immer construit toujours le prochain état à partir de zéro en fonction des changements que vous avez apportés au draft. Ça permet de garder des gestionnaires d’événements très concis sans jamais modifier l’état directement.

En résumé

  • Vous pouvez mettre des tableaux dans l’état, mais vous ne pouvez pas les modifier.
  • Au lieu de modifier un tableau, créez une nouvelle version de celui-ci et mettez à jour l’état avec cette nouvelle version.
  • Vous pouvez utiliser la syntaxe de spread de tableaux [...arr, newItem] pour créer des tableaux avec de nouveaux éléments.
  • Vous pouvez utiliser filter() et map() pour créer de nouveaux tableaux avec des éléments filtrés ou transformés.
  • Vous pouvez utiliser Immer pour garder votre code concis.

Défi 1 sur 4 ·
Mettre à jour un élément dans le panier

Complétez la logique de handleIncreaseClick de manière à ce que lorsqu’on appuie sur « + », la quantité de produit correspondante augmente :

import { useState } from 'react';

const initialProducts = [{
  id: 0,
  name: 'Baklava',
  count: 1,
}, {
  id: 1,
  name: 'Fromage',
  count: 5,
}, {
  id: 2,
  name: 'Spaghetti',
  count: 2,
}];

export default function ShoppingCart() {
  const [
    products,
    setProducts
  ] = useState(initialProducts)

  function handleIncreaseClick(productId) {

  }

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          {product.name}
          {' '}
          (<b>{product.count}</b>)
          <button onClick={() => {
            handleIncreaseClick(product.id);
          }}>
            +
          </button>
        </li>
      ))}
    </ul>
  );
}