Aggiornare gli Array nello State

In Javascript gli array sono mutabili, ma li dovresti trattare come immutabili quando li memorizzi nello state. Come per gli oggetti, quando vuoi aggiornare un array memorizzato nello state, devi crearne uno nuovo (o fare una copia di quello esistente) e impostare lo state per usare il nuovo array.

Imparerai

  • Come aggiungere, rimuovere o cambiare elementi in un array in React state
  • Come aggiornare un oggetto contenuto in un array
  • Come copiare gli array in maniera meno ripetitiva con Immer

Copiare gli array senza mutazione

In JavaScript, gli array sono solo un altro tipo di oggetto. Come per gli oggetti, in React state, dovresti trattare gli array come read-only. Questo significa che non dovresti riassegnare un elemento dentro un array come arr[0] = 'bird' e inoltre non dovresti usare metodi che mutano gli array, come push() e pop().

Invece, ogni volta che vuoi aggiornare un array, dovresti passare un nuovo array alla funzione setting dello state. Per fare ciò, puoi creare un nuovo array dall’array originale nello state richiamando i suoi metodi immutabili come filter() e map(). Infine puoi impostare il tuo state all’array che viene ritornato.

Qui c’è una tabella dove far riferimento per le più comuni operazioni sugli array. Quando hai a che fare con gli array contenuti in React state, dovrai evitare di utilizzare i metodi nella colonna di sinistra e, invece, preferire i metodi nella colonna di destra:

Evita (muta l’array)Preferisci (ritorna un nuovo array)
aggiuntapush, unshiftconcat, [...arr] sintassi spread (esempio)
rimozionepop, shift, splicefilter, slice (esempio)
sostituzionesplice, arr[i] = ... assegnazionemap (esempio)
ordinamentoreverse, sortprima copia l’array (esempio)

In alternativa, puoi usare Immer che ti permette di utilizzare i metodi di entrambe le colonne.

Insidia

Sfortunatamente, slice e splice sono chiamati in maniera simile ma sono molto differenti:

  • slice ti permette di copiare un array o parte di esso.
  • splice muta l’array (per inserire o eliminare elementi).

In React, dovresti usare slice (non p!) molto più spesso perché non vuoi mutare oggetti o array nello state. Aggiornare gli Oggetti spiega cos’è una mutazione e il perché non è consigliata per lo state.

Aggiungere ad un array

push() muta l’array, cosa che non vuoi:

import { useState } from 'react';

let nextId = 0;

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

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

Invece, crea un nuovo array che contiene gli elementi presenti e un nuovo elemento alla fine. Ci sono molteplici modi per raggiungere questo risultato, ma il più semplice è l’uso della sintassi ... array spread:

setArtists( // Sostituisce lo state
[ // con un nuovo array
...artists, // che contiene tutti i vecchi elementi
{ id: nextId++, name: name } // e ne aggiunge uno nuovo alla fine
]
);

Ora funziona correttamente:

import { useState } from 'react';

let nextId = 0;

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

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

La sintassi array spread ti consente inoltre di prepend (aggiungere) un elemento mettendolo prima dell’originale ...artists:

setArtists([
{ id: nextId++, name: name },
...artists // Mette i vecchi elementi alla fine
]);

In questo modo, lo spread fa il lavoro sia di push() aggiungendo un elemento alla fine dell’array sia di unshift() aggiungendo un elemento all’inizio di un array. Provalo nella sandbox sopra!

Rimozione da un array

Il modo più semplice per rimuovere un elemento dall’array è di filtrarlo via. In altre parole, devi creare un nuovo array che non conterrà quell’elemento. Per fare ciò, usa il metodo filter, per esempio:

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>Inspiring sculptors:</h1>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>
            {artist.name}{' '}
            <button onClick={() => {
              setArtists(
                artists.filter(a =>
                  a.id !== artist.id
                )
              );
            }}>
              Delete
            </button>
          </li>
        ))}
      </ul>
    </>
  );
}

Clicca il bottone “Delete” un paio di volte, e guarda il suo click handler.

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

Qui, artists.filter(a => a.id !== artist.id) significa “crea un array che è composto da questi artists i cui ID sono differenti da artist.id”. In altre parole, ogni bottone “Delete” dell’artista filtrerà via dall’array quel artista e infine ri-renderizzerà l’array ritornato. Tieni presente che filter non modifica l’array originale.

Trasformazione di un array

Se vuoi cambiare qualche o tutti gli elementi di un array, puoi usare map() per creare un nuovo array. La funzione che passerai a map può decidere cosa fare con ogni elemento, basandosi sui suoi dati o sul suo indice (o entrambi.)

In questo esempio, un array contiene delle coordinate di due cerchi e un quadrato. Quando premi il bottone, sposta in basso solotanto i cerchi di 50 pixel. Fa questo creando un nuovo array di dati usando 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') {
        // Nessun cambiamento
        return shape;
      } else {
        // Ritorna un nuovo cerchio in basso di 50px
        return {
          ...shape,
          y: shape.y + 50,
        };
      }
    });
    // Ri-renderizza con il nuovo array
    setShapes(nextShapes);
  }

  return (
    <>
      <button onClick={handleClick}>
        Move circles down!
      </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,
        }} />
      ))}
    </>
  );
}

Sostituzione degli elementi in un array

È particolarmente comune il voler sostituire uno o più elementi in un array. Le assegnazioni come arr[0] = 'bird' mutano l’array originale, invece dovresti utilizzare map anche in questo caso.

Per sostituire un elemento, crea un nuovo array con map. Dentro la tua chiamata map, riceverai l’indice di un elemento come secondo argomento. Usalo per decidere se ritornare l’elemento originale (il primo argomento) o qualcos’altro:

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) {
        // Incrementa il contatore cliccato
        return c + 1;
      } else {
        // Il resto non è cambiato
        return c;
      }
    });
    setCounters(nextCounters);
  }

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

Inserimento in un array

A volte, potresti voler inserire un elemento in una specifica posizione che non è nè all’inizio e nè alla fine. Per fare ciò, puoi usare la sintassi array spread ... assieme al metodo slice(). Il metodo slice() ti permette di tagliare una fetta (“slice”) dell’array. Per inserire un elemento, crea un array che metta (spreads) la fetta prima del punto di inserzione, poi il nuovo elemento e infine il resto dell’array originale.

In questo esempio, il bottone Insert inserisce sempre all’indice 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; // Può essere qualsiasi indice
    const nextArtists = [
      // Elementi prima del punto di inserzione:
      ...artists.slice(0, insertAt),
      // Nuovo elemento:
      { id: nextId++, name: name },
      // Elementi dopo il punto di inserzione:
      ...artists.slice(insertAt)
    ];
    setArtists(nextArtists);
    setName('');
  }

  return (
    <>
      <h1>Inspiring sculptors:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={handleClick}>
        Insert
      </button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

Altri cambiamenti ad un array

Ci sono determinate cose che non puoi fare utilizzando la sintassi spread e i metodi non mutanti come map() e filter() da soli. Per esempio, potresti voler invertire od ordinare un array. I metodi reverse() e sort() di Javascript mutano l’array originale, quindi non li puoi utilizzare direttamente.

Tuttavia, puoi prima copiare l’array e dopo applicare i cambiamenti.

Per esempio:

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}>
        Reverse
      </button>
      <ul>
        {list.map(artwork => (
          <li key={artwork.id}>{artwork.title}</li>
        ))}
      </ul>
    </>
  );
}

Qua, puoi usare la sintassi spread [...list] per creare prima una copia dell’array originale. Ora che hai una copia, puoi usare i metodi mutanti come nextList.reverse() o nextList.sort(), oppure puoi anche assegnare elementi individuali connextList[0] = "something".

Tuttavia, anche se copi un array, non puoi mutare gli elementi presenti dentro di esso direttamente. Questo perchè è una shallow copy (copia poco profonda)—il nuovo array conterrà gli stessi elementi dell’originale. Dunque, se modifichi un oggetto dentro un array copiato, stai mutando lo state esistente. Per esempio, codice come questo è un problema.

const nextList = [...list];
nextList[0].seen = true; //Problema: muta list[0]
setList(nextList);

Sebbene nextList e list siano due array differenti, nextList[0] e list[0] puntano allo stesso oggetto. Dunque, cambiando nextList[0].seen, stai anche cambiando list[0].seen. Questa è una mutazione dello state, che dovresti evitare! Puoi risolvere questo problema in maniera simile come aggiornare oggetti Javascript annidati— copiando individualmente gli elementi che vuoi cambiare invece di mutarli. Ecco come.

Aggiornare oggetti dentro agli array

Gli oggetti non sono veramente collocati “dentro” agli array. Dal codice potrebbero sembrare “dentro”, ma ogni oggetto in un array è un valore separato, al quale l’aray punta. Ecco perché devi fare attenzione quando cambi dei campi annidati come list[0]. Una lista di artwork di un’altra persona potrebbe puntare allo stesso elemento dell’array!

Quando aggiorni uno state annidato, hai bisogno di creare copie dal punto in cui vuoi aggiornarlo, fino al livello superiore. Vediamo come funziona.

In questo esempio, due liste di artwork separate hanno lo stesso state iniziale. Dovrebbero essere isolate, ma a causa della mutazione, il loro state è accidentalmente condiviso, e spuntando una casella in una lista, viene impattata anche l’altra lista:

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>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Your list of art to see:</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>
  );
}

Il problema è nel codice:

const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // Problem: mutates an existing item
setMyList(myNextList);

Sebbene lo stesso array myNextList sia nuovo, gli elementi stessi sono quelli dell’array originale myList. Quindi cambiare artwork.seen cambia l’originale elemento artwork. Quell’elemento artwork è presente anche inyourList, il quale causa il bug. I bugs come questo posso essere difficili da notare, ma fortunatamente scompaiono se eviti di mutare lo state.

Puoi usare map per sostituire un vecchio elemento con la sua versione aggiornata senza mutazioni

setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Crea un *nuovo* oggetto con gli aggiornamenti
return { ...artwork, seen: nextSeen };
} else {
// Nessun aggiornamento
return artwork;
}
}));

Qua, ... è la sintassi dello spread dell’oggetto usata per creare una copia dell’oggetto.

Con questo approccio, nessuno degli state esistenti viene mutat, e il bug è risolto:

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) {
        // Crea un *nuovo* oggetto con i cambi
        return { ...artwork, seen: nextSeen };
      } else {
        // Nessun cambio
        return artwork;
      }
    }));
  }

  function handleToggleYourList(artworkId, nextSeen) {
    setYourList(yourList.map(artwork => {
      if (artwork.id === artworkId) {
        // Crea un *nuovo* oggetto con i cambi
        return { ...artwork, seen: nextSeen };
      } else {
        // Nessun cambio
        return artwork;
      }
    }));
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Your list of art to see:</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>
  );
}

In generale, dovresti mutare solamente gli oggetti che hai appena creato. Se stessi inserendo un nuovo artwork, avresti potuto mutarlo, ma se stai avendo a che fare con qualcosa che è già presente nello state, hai bisogno di farne una copia.

Scrivi logica di aggiornamento coincisa con Immer

Aggiornare array annidati senza mutazione può essere un tantino ripetitivo. Come per gli oggetti:

  • Generalmente, non dovresti aver bisogno di aggiornare lo state per più di un paio di livelli di profondità. Se i tuoi oggetti dello state sono molto profondi, potresti volerli strutturare in maniera differente affinché siano piatti.
  • Se non vuoi che cambiare la struttura dello state, potresti preferire usare Immer, che ti permette di scrivere in modo conveniente ma con sintassi mutevole e generando le copie per te.

Questo è l’esempio dell’Art Bucket List riscritto con 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": {}
}

Nota come con Immer, le mutazioni come artwork.seen = nextSeen ora vadano bene:

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

Questo è perché non stai mutando lo state originale, ma stai mutando un oggetto speciale draft fornito da Immer. Analogamente, puoi applicare metodi di mutazione come push() e pop() al contenuto di draft.

Dietro le quinte, Immer costruisce sempre il prossimo state da zero in accordo con i cambiamenti che hai applicato a draft. Questo mantiene i tuoi event handler molto coincisi senza mai mutare lo state.

Riepilogo

  • Puoi mettere gli array nello state, ma non puoi cambiarli.
  • Invece di mutare un array, crea una nuova versione di esso e aggiorna lo state.
  • Puoi usare la sintassi array spread [...arr, newItem] per creare array con nuovi elementi.
  • Puoi usare filter() e map() per creare nuovi array con elementi filtrati o transformati.
  • Puoi usare Immer per mantenere il tuo codice coinciso.

Sfida 1 di 4:
Aggiorna un elemento nello shopping cart

Compila handleIncreaseClick così che premendo ”+” si increamenti il numero corrispondente:

import { useState } from 'react';

const initialProducts = [{
  id: 0,
  name: 'Baklava',
  count: 1,
}, {
  id: 1,
  name: 'Cheese',
  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>
  );
}