Taulukkojen päivittäminen tilassa
Taulukot ovat mutatoitavissa, mutta niitä tulisi käsitellä kuin ne olisivat ei-mutatoitavissa kun tallennat niitä tilaan. Kuten olioiden kanssa, päivitä tilaan tallennettu taulukko luomalla uusi (tai tekemällä kopio vanhasta) ja sitten aseta tila käyttämään uutta taulukkoa.
Tulet oppimaan
- Miten lisätä, poistaa, tai muuttaa taulukon kohteita Reactin tilassa
- Miten päivittää taulukon sisällä olevaa oliota
- Miten teet taulukoiden kopioimisesta vähemmän toistuvaa Immerillä
Taulukkojen päivittäminen ilman mutaatiota
JavaScriptissa taulukot ovat kuin toisenlainen olio. Kuten olioiden kanssa, sinun tulisi käsitellä Reactin tilan taulukkoja vain-luku muodossa. Tämä tarkoittaa, että sinun ei pitäisi uudelleen määritellä taulukon kohteita arr[0] = 'bird'
tavalla eikä kannattaisi käyttää mutatoivia tapoja muokata taulukkoa, kuten push()
ja pop()
.
Sen sijaan, joka kerta kun haluat päivittää taulukkoa, haluat välittää uuden taulukon tilan asettajafunktiolle. Voit tehdä tämän luomalla uuden taulukon alkuperäisestä taulukosta kutsumalla sen ei-mutatoivia metodeja kuten filter()
ja map()
. Sitten voit asettaa uuden taulukon tilaksi.
Tässä on viitetaulukko yleisistä taulukon toiminnoista. Kun käsittelet taulukoita Reactin tilassa, sinun pitäisi välttää metodeja taulukon vasemmalla sarakkeella, ja sen sijaan suosia metodeja taulukon oikealla sarakkeella:
vältä (mutatoi taulukkoa) | suosi (palauttaa uuden taulukon) | |
---|---|---|
lisääminen | push , unshift | concat , [...arr] spread syntaksi (esimerkki) |
poistaminen | pop , shift , splice | filter , slice (esimerkki) |
korvaaminen | splice , arr[i] = ... määrittely | map (esimerkki) |
järestäminen | reverse , sort | kopioi taulukko ensin (esimerkki) |
Vaihtoehtoisesti, voit käyttää Immeriä, jonka avulla voit käyttää metodeja molemmista sarakkeista.
Taulukkoon lisääminen
push()
funktio tekee mutaation, jota et halua:
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> </> ); }
Sen sijaan luo uusi taulukko, joka sisältää aiemman taulukonkohteet ja uuden kohteen taulukon lopussa. Tämän toteuttamiseen on useita tapoja, mutta helpoin on käyttää ...
array spread syntaksia:
setArtists( // Korvaa tila
[ // uudella taulukolla
...artists, // joka sisältää vanhat kohteet
{ id: nextId++, name: name } // ja yhden uuden lopussa
]
);
Nyt se toimii oikein:
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> </> ); }
Taulukon spread syntaksilla voit myös lisätä kohteen taulukon alkuun, sijoittamalla sen ennen alkuperäistä ...artists
taulukkoa:
setArtists([
{ id: nextId++, name: name },
...artists // Aseta vanhat kohteet loppuun
]);
Näin spread syntaksi hoitaa sekä push()
funktion että unshift()
funktion työt. Kokeile yllä olevassa hiekkalaatikossa!
Taulukosta poistaminen
Helpoin tapa poistaa kohde taulukosta on suodattamalla se pois. Toisin sanoen, luot uuden taulukon, joka ei sisällä poistettavaa kohdetta. Voit tehdä tämän käyttämällä filter
metodia, esimerkiksi:
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> </> ); }
Klikkaa “Delete” painiketta muutaman kerran ja tarkastele sen klikkauskäsittelijää.
setArtists(
artists.filter(a => a.id !== artist.id)
);
Tässä artists.filter(a => a.id !== artist.id)
tarkoittaa “luo uusi taulukko, joka koostuu artists
kohteista, joiden ID:t ovat eri kuin artist.id
”. Toisin sanoen, jokaisen artistin “Delete” painike suodattaa juuri sen artistin pois taulukosta, ja sitten pyytävät uudelleen renderöintiä lopullisella taulukolla. Huomaa, että filter
ei muokkaa olemassa olevaa taulukkoa.
Taulukon muuntaminen
Jos haluat muuntaa joitakin tai kaikkia taulukon kohteita, voit käyttää map()
metodia luodaksesi uuden taulukon. Funktion, jonka välität map
metodille voi määritellä mitä teet kullekin kohteelle sen datan tai indeksin (tai molempien) pohjalta.
Tässä esimerkissä taulukko sisältää koordinaatit kahdelle ympyrälle ja yhdelle neliölle. Kun painat painiketta, se siirtää vain ympyröitä 50 pikseliä alaspäin. Se tekee tämän luomalla uuden taulukon käyttäen map()
funktiota:
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') { // No change return shape; } else { // Return a new circle 50px below return { ...shape, y: shape.y + 50, }; } }); // Re-render with the new 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, }} /> ))} </> ); }
Taulukon kohteiden korvaaminen
On yleistä, että haluat korvata yhden tai useamman kohteen taulukossa.
It is particularly common to want to replace one or more items in an array. Määritykset kuten arr[0] = 'bird'
mutatoivat alkuperäistä taulukkoa, joten sen sijaan voit käyttää map
metodia myös tähän.
Korvataksesi kohteen, luo uusi taulukko map
:lla. map
kutsun sisälle vastaanotat kohteen indeksin toisena argumenttina. Käytä sitä päättämään, palautetaanko alkuperäinen kohde (ensimmäinen argumentti) vai jotain muuta:
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) { // Increment the clicked counter return c + 1; } else { // The rest haven't changed return c; } }); setCounters(nextCounters); } return ( <ul> {counters.map((counter, i) => ( <li key={i}> {counter} <button onClick={() => { handleIncrementClick(i); }}>+1</button> </li> ))} </ul> ); }
Tiettyyn kohtaan lisääminen
Joskus saatat haluta sijoittaa kohteen tiettyyn kohtaan, joka ei kuitenkaan ole taulukon alussa tai lopussa. Voit tehdä tämän käyttämällä ...
syntaksia yhdessä slice()
metodin kanssa. slice()
metodi antaa sinun leikata “palan” taulukosta. Sijoittaaksesi kohteen, luot uuden taulukon joka levittää “palan” ennen sijoituskohtaa, sitten uuden kohteen, ja lopuksi loput alkuperäisestä taulukosta.
Tässä esimerkissä, “Insert” painike sijoittaa aina indeksiin 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; // Could be any index const nextArtists = [ // Items before the insertion point: ...artists.slice(0, insertAt), // New item: { id: nextId++, name: name }, // Items after the insertion point: ...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> </> ); }
Muiden muutosten tekeminen taulukkoihin
On joitakin asioita, joita et voi tehdä pelkällä spread syntaksilla ja ei-mutatoivilla metodeilla kuten map()
:lla ta
filter():lla. Esimerkiksi, saatat haluta kääntää taulukon järjestyksen tai suodattaa taulukkoa. JavaScriptin
reverse()- ja
sort()`-metodit muuttavat alkuperäistä taulukkoa, joten niitä ei voi käyttää suoraan.
Kuitenkin, voit kopioida taulukon ensiksi ja sitten tehdä muutoksia siihen.
Esimerkiksi:
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}> Reverse </button> <ul> {list.map(artwork => ( <li key={artwork.id}>{artwork.title}</li> ))} </ul> </> ); }
Tässä käytät [...list]
spread syntaksia luodaksesi kopion alkuperäisestä taulukosta ensiksi. Nyt kun kopio on luotu, voit käyttää mutatoivia metodeja kuten nextList.reverse()
tai nextList.sort()
, tai jopa määrittää yksittäisiä kohteita nextList[0] = "something"
määrittelyllä.
Kuitenkin, vaikka kopioisit taulukon, et voi mutatoida sen sisällä olevia kohteita suoraan. Tämä siksi koska kopiointi on pinnallista—uusi taulukko sisältää samat kohteet kuin alkuperäinen. Joten jos muokkaat kopioidun taulukon sisällä olevia olioita, mutatoit olemassa olevaa tilaa. Esimerkiksi, alla oleva koodi on ongelmallista.
const nextList = [...list];
nextList[0].seen = true; // Ongelma: mutatoi list[0] kohdetta
setList(nextList);
Vaikka nextList
ja list
ovat kaksi eri taulukkoa, nextList[0]
ja list[0]
osoittavat samaan olioon. Joten muuttamalla nextlist[0].seen
kohdetta muutat myös list[0].seen
kohdetta. Tämä on tilanmuutos, jota tulisi välttää! Voit ratkaista ongelman samalla tavalla kuin sisäkkäisten olioiden päivittäminen—eli kopioimalla yksittäiset kohteet, joita haluat muuttaa mutatoinnin sijaan. Näin se onnistuu.
Olioiden päivittäminen taulukon sisällä
Oliot eivät oikeasti sijaitse taulukkojen “sisällä”. Ne saattavat näyttäytyä olevan “sisällä” koodissa, mutta jokainen olio taulukossa on erillinen arvo johon taulukko “osoittaa”. Tämän takia täytyy olla tarkkana kun muutat sisäkkäisiä kenttiä kuten list[0]
. Toisen henkilön taideteoslista saattaa osoittaa samaan kohteeseen taulukossa!
Päivittäessä sisäkkäistä tilaa, sinun täytyy luoda kopioita siihen pisteeseen saakka mitä haluat päivittää, ja aina ylätasoon asti. Katsotaan miten tämä toimii.
Tässä esimerkissä, kahdella erillisellä taideteoslistalla on sama aloitustila. Niiden on tarkoitus olla eristettyinä, mutta mutaation seurauksena niiden tila on vahingossa jaettu. Valintaruudun valitseminen yhdessä listassa vaikuttaa toiseen listaan:
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> ); }
Ongelma on seuraavassa koodissa:
const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // Ongelma: mutatoi olemassa olevaa kohdetta
setMyList(myNextList);
Vaikka myNextList
taulukko on uusi, kohteet ovat samat kuin alkuperäisessä myList
taulukossa. Joten artwork.seen
:n muuttaminen muuttaa alkuperäistä taideteoskohdetta. Tuo taideteos on myös yourList
taulukossa, joka aiheuttaa bugin. Tällaisia bugeja voi olla vaikea ajatella, mutta onneksi ne katoavat jos vältät tilan mutatointia.
Voit käyttää map
metodia korvataksesi vanhan kohteen sen päivitetyllä versiolla ilman mutatointia.
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Luo *uusi* olio muutoksilla
return { ...artwork, seen: nextSeen };
} else {
// Ei muutosta
return artwork;
}
}));
Tässä, ...
on olion levityssyntaksi, jota käytetään uuden kopion luomiseksi.
With this approach, none of the existing state items are being mutated, and the bug is fixed:
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) { // Create a *new* object with changes return { ...artwork, seen: nextSeen }; } else { // No changes return artwork; } })); } function handleToggleYourList(artworkId, nextSeen) { setYourList(yourList.map(artwork => { if (artwork.id === artworkId) { // Create a *new* object with changes return { ...artwork, seen: nextSeen }; } else { // No changes 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> ); }
Yleisesti ottaen, sinun tulisi mutatoida oliota joita olet juuri luonut. Jos olet sijoittamassa uutta taideteosta, voisit mutatoida taulukkoa, mutta jos käsittelet jotain, joka on jo tilassa, sinun täytyy tehdä kopio.
Kirjoita tiivis päivityslogiikka Immerillä
Sisennettyjen taulukoiden päivittäminen ilman mutaatiota saattaa koitua toistuvaksi. Juuri kuten olioiden kanssa:
- Yleisesti ottaen sinun ei tulisi päivittää tilaa kahta tasoa syvemmältä. Jos tilaoliosi ovat todella syviä, saatat haluta järjestää ne eri tavalla, jotta ne olisivat tasaisia.
- Jos et haluat muuttaa tilasi rakennetta, saatat pitää Immer:stä, jonka avulla voit kirjoittaa kätevällä mutta mutatoivalla syntaksilla, hoitaen kopiot puolestasi.
Tässä on Art Bucket List esimerkki uudelleenkirjoitettuna Immerillä:
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>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> ); }
Huomaa miten Immerillä mutatointi kuten artwork.seen = nextSeen
on nyt sallittua:
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
Tämä siksi koska et mutatoi alkuperäistä tilaa, vaan mutatoit erityistä draft
oliota, jonka Immer tarjoaa. Vastaavasti, voit käyttää mutatoivia metodeja kuten push()
ja pop()
draft
olion sisällöille.
Konepellin alla Immer luo aina uuden tilan alusta pohjautuen muutoksiin, joita teit draft
oliolle. Tämä pitää Tapahtumankäsittelijäsi todella tiiviinä mutatoimatta tilaa.
Kertaus
- Voit laittaa taulukkoja tilaan, mutta et voi muuttaa niitä.
- Mutatoinnin sijaan luot uuden version siitä ja päivität tilan vastaamaan sitä.
- Voit käyttää
[...arr, newItem]
array levityssyntaksia luodaksesi taulukon uusilla kohteilla. - Voit käyttää
filter()
jamap()
metodeja luodaksesi uuden taulukon suodatetuilla tai muunneltuilla kohteilla. - Voit käyttää Immeriä pitääksesi koodin tiivinä.
Haaste 1 / 4: Päivitä kohdetta ostoskorissa
Täytä handleIncreaseClick
:n logiikka, jotta ”+“:n painaminen kasvattaa vastaavaa numeroa:
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> ); }