Ažuriranje nizova u state-u
Nizovi su promenljivi u JavaScript-u, ali biste trebali da ih tretirate kao immutable kada ih čuvate u state-u. Kao i kod objekata, kada želite da ažurirate niz koji se nalazi u state-u, potrebno je da kreirate novi (ili napravite kopiju postojećeg), a zatim postavite state da koristi taj novi niz.
Naučićete:
- Kako da dodajete, uklanjate ili menjate članove niza u React state-u
- Kako da ažurirate objekat unutar niza
- Kako učiniti da se kopiranje nizova manje ponavlja uz pomoć Immer-a
Ažuriranje nizova bez mutacije
U JavaScript-u, nizovi su samo još jedna vrsta objekata. Kao i sa objektima, trebali biste tretirati nizove u React state-u kao read-only. To znači da ne trebate dodeljivati vrednost članovima niza poput arr[0] = 'bird'
, a takođe ne biste trebali da koristite metode koje mutiraju niz, kao što su push()
i pop()
.
Umesto toga, svaki put kada želite ažurirati niz, želećete da prosledite novi niz u vašu state setter funkciju. Da biste to uradili, možete kreirati novi niz na osnovu originalnog niza u state-u pozivanjem metoda koje ne vrše mutaciju poput filter()
i map()
. Onda možete postaviti state na novi rezultujući niz.
Ovde je referentna tabela uobičajenih operacija nad nizovima. Kada koristite nizove unutar React state-a, trebate izbegavati metode u levoj koloni i, umesto toga, koristiti metode iz desne kolone:
izbegavati (mutira niz) | koristiti (vraća novi niz) | |
---|---|---|
dodavanje | push , unshift | concat , [...arr] spread sintaksa (primer) |
uklanjanje | pop , shift , splice | filter , slice (primer) |
zamena | splice , arr[i] = ... dodela | map (primer) |
sortiranje | reverse , sort | prvo kopirati niz (primer) |
Alternativno, možete koristiti Immer koji vam dopušta da koristite metode iz obe kolone.
Dodavanje u niz
push()
ce mutirati niz, a to ne želite:
import { useState } from 'react'; let nextId = 0; export default function List() { const [name, setName] = useState(''); const [artists, setArtists] = useState([]); return ( <> <h1>Inspirativni skulptori:</h1> <input value={name} onChange={e => setName(e.target.value)} /> <button onClick={() => { artists.push({ id: nextId++, name: name, }); }}>Dodaj</button> <ul> {artists.map(artist => ( <li key={artist.id}>{artist.name}</li> ))} </ul> </> ); }
Umesto toga, napravite novi niz koji sadrži sve postojeće članove, ali i novi član na kraju. Postoji više načina da to uradite, ali najlakši je upotrebom ...
spread sintakse za nizove:
setArtists( // Zameni state
[ // sa novim nizom
...artists, // koji sadrži sve stare članove
{ id: nextId++, name: name } // kao i novi član na kraju
]
);
Sada radi kako treba:
import { useState } from 'react'; let nextId = 0; export default function List() { const [name, setName] = useState(''); const [artists, setArtists] = useState([]); return ( <> <h1>Inspirativni skulptori:</h1> <input value={name} onChange={e => setName(e.target.value)} /> <button onClick={() => { setArtists([ ...artists, { id: nextId++, name: name } ]); }}>Dodaj</button> <ul> {artists.map(artist => ( <li key={artist.id}>{artist.name}</li> ))} </ul> </> ); }
Spread sintaksa za nizove omogućava i dodavanje članova pre originalnih ...artists
:
setArtists([
{ id: nextId++, name: name },
...artists // Stavi stare članove na kraj
]);
Na ovaj način, spread može obavljati oba posla: push()
- dodavanje na kraj niza i unshift()
- dodavanje na početak niza. Probajte u sandbox-u iznad!
Uklanjanje iz niza
Najlakši način za uklanjanje člana iz niza je da ga isfiltrirate. Drugim rečima, napravićete novi niz koji ne sadrži taj član. Da biste to uradili, koristite filter
metodu, na primer:
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>Inspirativni skulptori:</h1> <ul> {artists.map(artist => ( <li key={artist.id}> {artist.name}{' '} <button onClick={() => { setArtists( artists.filter(a => a.id !== artist.id ) ); }}> Ukloni </button> </li> ))} </ul> </> ); }
Kliknite dugme “Ukloni” par puta i pogledajte njegov klik handler.
setArtists(
artists.filter(a => a.id !== artist.id)
);
Ovde, artists.filter(a => a.id !== artist.id)
znači “napravi niz koji sadrži sve artists
čiji ID je različit od artist.id
”. Drugim rečima, “Ukloni” dugme kod svakog umetnika će isfiltrirati tog umetnika van niza, a onda zatražiti ponovni render sa rezultujućim nizom. Primetite da filter
ne menja originalni niz.
Transformisanje niza
Ako želite da promenite neki ili sve članove niza, možete koristiti map()
da kreirate novi niz. Funkcija koju prosledite u map
može odlučiti šta uraditi za svaki član, na osnovu njegovih podataka ili indeksa (ili oba).
U ovom primeru, niz sadrži koordinate dva kruga i jednog kvadrata. Kada pritisnete dugme, samo će se krugovi pomeriti na dole za 50 piksela. To se radi pravljenjem novog niza upotrebom map()
metode:
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') { // Nema promene return shape; } else { // Vrati novi krug 50px ispod return { ...shape, y: shape.y + 50, }; } }); // Ponovni render sa novim nizom setShapes(nextShapes); } return ( <> <button onClick={handleClick}> Pomeri krugove dole! </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, }} /> ))} </> ); }
Zamena članova niza
Dosta je uobičajeno da želite zameniti jedan ili više članova niza. Dodele poput arr[0] = 'bird'
mutiraju originalni niz, tako da ćete umesto toga želeti i ovde da koristite map
.
Da biste zamenili član, napravite novi niz sa map
. Unutar map
poziva, dobićete indeks člana kao drugi argument. Iskoristite ga da odlučite da li da vratite originalni član (prvi argument) ili nešto drugo:
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) { // Inkrementiraj kliknuti brojač return c + 1; } else { // Ostatak se ne menja return c; } }); setCounters(nextCounters); } return ( <ul> {counters.map((counter, i) => ( <li key={i}> {counter} <button onClick={() => { handleIncrementClick(i); }}>+1</button> </li> ))} </ul> ); }
Ubacivanje u niz
Ponekad ćete želeti da ubacite član u niz na određenu poziciju koja nije ni početak ni kraj. Da biste to uradili, možete koristiti ...
spread sintaksu za nizove zajedno sa slice()
metodom. slice()
metoda vam omogućava da uzmete “parče” niza. Da biste ubacili član, kreiraćete niz koji sadrži parče pre mesta ubacivanja, novi član i ostatak originalnog niza.
U ovom primeru, dugme “Ubaci” uvek ubacuje na indeks 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; // Može biti bilo koji indeks const nextArtists = [ // Članovi pre mesta ubacivanja: ...artists.slice(0, insertAt), // New item: { id: nextId++, name: name }, // Članovi nakon mesta ubacivanja: ...artists.slice(insertAt) ]; setArtists(nextArtists); setName(''); } return ( <> <h1>Inspirativni skulptori:</h1> <input value={name} onChange={e => setName(e.target.value)} /> <button onClick={handleClick}> Ubaci </button> <ul> {artists.map(artist => ( <li key={artist.id}>{artist.name}</li> ))} </ul> </> ); }
Pravljenje ostalih izmena nad nizom
Postoje stvari koje ne možete uraditi samo sa spread sintaksom i metodama koje ne vrše mutacije poput map()
i filter()
. Na primer, možete želeti da obrnete ili sortirate niz. JavaScript reverse()
i sort()
metode mutiraju originalni niz, pa ih ne možete koristiti direktno.
Međutim, možete prvo kopirati niz, pa onda praviti promene nad njim.
Na primer:
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}> Obrni </button> <ul> {list.map(artwork => ( <li key={artwork.id}>{artwork.title}</li> ))} </ul> </> ); }
Ovde prvo koristite [...list]
spread sintaksu da napravite kopiju originalnog niza. Sad kad imate kopiju, možete koristiti metode koje mutiraju kao što su nextList.reverse()
ili nextList.sort()
, a možete i vršiti dodelu određenim članovima sa nextList[0] = "something"
.
Međutim, čak i ako kopirate niz, ne možete mutirati postojeće članove unutar njega direktno. Ovo je zbog toga što je kopija plitka—novi niz će sadržati iste članove kao i originalni. Ako izmenite objekat unutar kopiranog niza, vi mutirate postojeći state. Na primer, kod poput ovog je problematičan.
const nextList = [...list];
nextList[0].seen = true; // Problem: mutira list[0]
setList(nextList);
Iako su nextList
i list
dva različita niza, nextList[0]
i list[0]
pokazuju na isti objekat. To znači da promenom nextList[0].seen
, takođe menjate i list[0].seen
. Ovo je mutacija state-a, što trebate izbegavati! Ovaj problem možete rešiti na sličan način kao i ažuriranje ugnježdenih JavaScript objekata—umesto mutacije, možete kopirati pojedinačne članove koje želite da promenite. Evo i kako.
Ažuriranje objekata unutar nizova
Objekti se ne nalaze zapravo “unutar” nizova. U kodu može delovati kao da su “unutar”, ali svaki objekat u nizu je posebna vrednost na koju niz “pokazuje”. Zbog toga trebate biti oprezni kad menjate ugnježdena polja poput list[0]
. Lista umetničkih dela druge osobe može pokazivati na isti element niza!
Kada ažurirate ugnježdeni state, trebate kreirati kopije od mesta gde želite ažurirati, pa nagore sve do najvišeg nivoa. Hajde da vidimo kako to funkcioniše.
U ovom primeru, dve različite liste umetničkih dela imaju isti inicijalni state. Trebalo bi da su izolovane, ali, zbog mutacije, slučajno dele state, pa štikliranje u jednoj listi utiče i na drugu:
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>Lista željenih umetnosti</h1> <h2>Moja lista:</h2> <ItemList artworks={myList} onToggle={handleToggleMyList} /> <h2>Tvoja lista:</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> ); }
Problem je u ovom kodu:
const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // Problem: mutira postojeći član
setMyList(myNextList);
Iako je sam myNextList
niz novi, članovi u njemu su isti kao u originalnom myList
nizu. Promenom artwork.seen
menjate originalno umetničko delo. To umetničko delo je i u yourList
, što prouzrokuje bug. Može biti teško razmišljati o ovakvim bug-ovima, ali, srećom, oni nestaju ako izbegavate mutiranje state-a.
Možete koristiti map
da zamenite stari član sa njegovom ažuriranom verzijom bez mutacije.
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Napravi *novi* objekat sa promenama
return { ...artwork, seen: nextSeen };
} else {
// Nema promena
return artwork;
}
}));
Ovde, ...
je objektna spread sintaksa kojom se kreira kopija objekta.
Ovim pristupom se ne mutira nijedan član postojećeg state-a i popravlja se bug:
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) { // Napravi *novi* objekat sa promenama return { ...artwork, seen: nextSeen }; } else { // Nema promena return artwork; } })); } function handleToggleYourList(artworkId, nextSeen) { setYourList(yourList.map(artwork => { if (artwork.id === artworkId) { // Napravi *novi* objekat sa promenama return { ...artwork, seen: nextSeen }; } else { // Nema promena return artwork; } })); } return ( <> <h1>Lista željenih umetnosti</h1> <h2>Moja lista:</h2> <ItemList artworks={myList} onToggle={handleToggleMyList} /> <h2>Tvoja lista:</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> ); }
Uglavnom, trebali biste mutirati samo objekte koje ste upravo kreirali. Ako ubacujete novo umetničko delo, možete ga mutirati, ali ako menjate nešto što je već u state-u, trebate napraviti kopiju.
Pisanje koncizne logike ažuriranja sa Immer-om
Ažuriranje ugnježdenih nizova bez mutacije može postati ponovljivo. Baš kao i sa objektima:
- Generalno, ne biste trebali da ažurirate state za više od par nivoa dubine. Ako su vaši state objekti dosta hijerarhijski duboki, možete probati da ih drugačije strukturirate kako bi bili flat.
- Ako ne želite promeniti strukturu vašeg state-a, možda biste voleli da koristite Immer koji omogućava pisanje zgodne sintakse za mutiranje i brine o pravljenju kopija za vas.
Ovde je primer Liste željenih umetnosti napisan pomoću Immer-a:
{ "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": {} }
Primetite kako je sa Immer-om, mutacija poput artwork.seen = nextSeen
sada u redu:
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
To se dešava zato što ne mutirate originalni state, već poseban draft
objekat koji Immer pruža. Slično tome, možete upotrebiti mutirajuće metode poput push()
i pop()
nad draft
objektom.
Iza kulisa, Immer uvek konstruiše novi state od nule na osnovu promena koje ste izvršili nad draft
-om. Ovo vaše event handler-e čini konciznijim bez mutiranja state-a.
Recap
- Možete držati nizove u state-u, ali ih ne smete menjati.
- Umesto da mutirate niz, napravite novu verziju niza i ažurirajte state da je koristi.
- Možete koristiti
[...arr, newItem]
spread sintaksu za nizove da napravite nizove sa novim članovima. - Možete koristiti
filter()
imap()
za kreiranje novih nizova sa filtriranim i transformisanim članovima. - Možete koristiti Immer da vam kod bude koncizniji.
Izazov 1 od 4: Ažurirati proizvod u korpi za kupovinu
Popunite handleIncreaseClick
metodu tako da pritiskanje ”+” dugmeta inkrementira odgovarajući broj:
import { useState } from 'react'; const initialProducts = [{ id: 0, name: 'Baklava', count: 1, }, { id: 1, name: 'Sir', count: 5, }, { id: 2, name: 'Špagete', 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> ); }