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) | |
---|---|---|
aggiunta | push , unshift | concat , [...arr] sintassi spread (esempio) |
rimozione | pop , shift , splice | filter , slice (esempio) |
sostituzione | splice , arr[i] = ... assegnazione | map (esempio) |
ordinamento | reverse , sort | prima copia l’array (esempio) |
In alternativa, puoi usare Immer che ti permette di utilizzare i metodi di entrambe le colonne.
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()
emap()
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> ); }