Mettre à jour les tableaux d'un état

Les tableaux sont un autre 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 mutation

En JavaScript, les tableaux sont simplement un autre type d’objet. Tout comme avec les objets, vous devez considérer les tableaux dans l’état React comme étant en lecture seule. Cela signifie que vous ne devez pas réassigner les éléments à l’intérieur d’un tableau, comme arr[0] = 'bird', et vous ne devez pas non plus utiliser des méthodes qui modifient le tableau, 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, vous devrez éviter les méthodes de la colonne de gauche et privilégier 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 les objets ou les tableaux dans l’état. Mise à jour des objets explique ce qu’est la mutation et pourquoi elle n’est pas recommandée pour l’état.

Ajouter un élément à un tableau

push() va modifier 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 faire cela, mais la plus simple est d’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>
    </>
  );
}

L’opérateur 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-le dans le bac à sable ci-dessus !

Supprimer un élément d’un tableau

Le moyen le plus simple de supprimer un élément d’un tableau est de 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, 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éer un tableau qui se compose des 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 certains ou tous les é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, les cercles seront déplacés de 50 pixels vers le bas. Cela est réalisé 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] = 'bird' 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 spread 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';

let nextId = 3;
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 modifierez 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 de l’objet. Voyons comment cela 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 mutation, 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 de souhaits d'art</h1>
      <h2>Ma liste d'œuvres d'art à voir :</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Ta liste d'œuvres d'art à voir :</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 un 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 lui-même 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’artwork d’origine. Cet artwork 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éez 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 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éez 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éez un *nouvel* objet avec les modifications
        return { ...artwork, seen: nextSeen };
      } else {
        // Pas de changement
        return artwork;
      }
    }));
  }

  return (
    <>
      <h1>Liste de souhaits d'art</h1>
      <h2>Ma liste d'œuvres d'art à voir :</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Ta liste d'œuvres d'art à voir :</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 mutation peut devenir 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 plats.
  • Si vous ne souhaitez pas changer la structure de votre état, vous pouvez 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 à voir réécrit avec Immer :

import { useState } from 'react';
import { useImmer } from 'use-immer';

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, updateMyList] = useImmer(
    initialList
  );
  const [yourList, updateYourList] = useImmer(
    initialList
  );

  function handleToggleMyList(id, nextSeen) {
    updateMyList(draft => {
      const artwork = draft.find(a =>
        a.id === id
      );
      artwork.seen = nextSeen;
    });
  }

  function handleToggleYourList(artworkId, nextSeen) {
    updateYourList(draft => {
      const artwork = draft.find(a =>
        a.id === artworkId
      );
      artwork.seen = nextSeen;
    });
  }

  return (
    <>
      <h1>Liste de souhaits d'art</h1>
      <h2>Ma liste d'œuvres à voir :</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Votre liste d'œuvres à voir :</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>
  );
}

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;
});

Cela est possible car vous ne modifiez pas l’état d’origine, mais vous modifiez 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. Cela permet de maintenir vos gestionnaires d’événements très concis sans jamais modifier l’état.

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 en conséquence.
  • Vous pouvez utiliser la syntaxe de spread [...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 lorsque l’on appuie sur ”+”, le nombre correspondant 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>
  );
}