Ažuriranje objekata u state-u
State može sadržati bilo koju vrstu JavaScript vrednosti, uključujući i objekte. Međutim, ne bi trebalo direktno menjati objekte koje držite u React state-u. Umesto toga, kada želite da ažurirate objekat, potrebno je da kreirate novi (ili napravite kopiju postojećeg) i zatim ažurirate state kako bi koristio tu kopiju.
Naučićete:
- Kako korektno ažurirati objekat u React state-u
- Kako ažurirati ugnježdeni objekat bez mutiranja
- Šta je immutability i kako da je ne prekršite
- Kako učiniti da se kopiranje objekta manje ponavlja uz pomoć Immer-a
Šta je to mutacija?
Bilo koju vrstu JavaScript vrednosti možete držati u state-u.
const [x, setX] = useState(0);
Do sad ste radili sa brojevima, stringovima i boolean vrednostima. Te vrste JavaScript vrednosti su “immutable”, što znači nepromenljivo ili “read-only”. Možete pokrenuti ponovni render da zamenite vrednost:
setX(5);
State x
se promenilo sa 0
na 5
, ali sam broj 0
se nije promenio. Nije moguće napraviti bilo kakve promene nad ugrađenim primitivnim vrednostima poput brojeva, stringova ili boolean-a u JavaScript-u.
Razmotrite objekat u state-u:
const [position, setPosition] = useState({ x: 0, y: 0 });
Tehnički, moguće je promeniti sadržaj samog objekta. Ovo se naziva mutacija:
position.x = 5;
Međutim, iako su objekti u React state-u tehnički mutable (promenljivi), trebalo bi da ih tretirate kao da su immutable—poput brojeva, boolean-a i stringova. Umesto da ih mutirate, uvek ih trebate zameniti.
Tretiranje state-a kao read-only
Drugim rečima, trebate bilo koji JavaScript objekat koji držite u state-u da tretirate kao da je read-only.
Ovaj primer u state-u drži objekat koji predstavlja trenutnu poziciju kursora. Crvena tačka bi trebala da se pomera kada kliknete ili pomerate kursor preko preview oblasti. Ali, tačka ostaje na svojoj inicijalnoj poziciji:
import { useState } from 'react'; export default function MovingDot() { const [position, setPosition] = useState({ x: 0, y: 0 }); return ( <div onPointerMove={e => { position.x = e.clientX; position.y = e.clientY; }} style={{ position: 'relative', width: '100vw', height: '100vh', }}> <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ); }
Problem je u ovom delu koda.
onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
}}
Ovaj kod menja objekat dodeljen position
promenljivoj iz prethodnog rendera. Ali, bez upotrebe state setter funkcije, React nema predstavu da se objekat promenio. Zato React ne radi ništa. Ovo je kao da pokušate promeniti porudžbinu nakon što završite obrok. Iako mutacija state-a može raditi u nekim slučajevima, ne preporučujemo takav pristup. Trebate tretirati state vrednost koju imate u renderu kao read-only.
Da biste zapravo pokrenuli ponovni render u ovom slučaju, kreirajte novi objekat i prosledite ga u state setter funkciju:
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}
Sa setPosition
React-u govorite sledeće:
- Zameni
position
sa novim objektom - I renderuj komponentu ponovo
Primetite da crvena tačka sad prati vaš kursor kada ga pomerate kroz preview oblast:
import { useState } from 'react'; export default function MovingDot() { const [position, setPosition] = useState({ x: 0, y: 0 }); return ( <div onPointerMove={e => { setPosition({ x: e.clientX, y: e.clientY }); }} style={{ position: 'relative', width: '100vw', height: '100vh', }}> <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ); }
Deep Dive
Kod poput ovog je problematičan jer menja postojeći objekat u state-u:
position.x = e.clientX;
position.y = e.clientY;
Ali, ovakav kod je apsolutno u redu jer mutirate novi objekat koji ste upravo kreirali:
const nextPosition = {};
nextPosition.x = e.clientX;
nextPosition.y = e.clientY;
setPosition(nextPosition);
U suštini, potpuno je jednako ovome:
setPosition({
x: e.clientX,
y: e.clientY
});
Mutacija je jedino problematična kada menjate postojeće objekte koji su već u state-u. Mutiranje objekta koji ste upravo kreirali je u redu zato što ga ništa ne referencira još uvek. Njegovom promenom ne možete slučajno uticati na nešto što zavisi od njega. Ovo se naziva “lokalna mutacija”. Možete koristiti lokalnu mutaciju i tokom renderovanja. Veoma zgodno i potpuno u redu!
Kopiranje objekata sa spread sintaksom
U prethodnom primeru, position
objekat se uvek kreirao iznova na osnovu trenutne pozicije kursora. Ali, često ćete želeti da uključite postojeće podatke u novi objekat koji kreirate. Na primer, možete želeti da ažurirate samo jedno polje u formi, a da ostavite trenutne vrednosti za ostala polja.
Ova input polja ne rade jer onChange
handler-i mutiraju state:
import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com' }); function handleFirstNameChange(e) { person.firstName = e.target.value; } function handleLastNameChange(e) { person.lastName = e.target.value; } function handleEmailChange(e) { person.email = e.target.value; } return ( <> <label> Ime: <input value={person.firstName} onChange={handleFirstNameChange} /> </label> <label> Prezime: <input value={person.lastName} onChange={handleLastNameChange} /> </label> <label> Email: <input value={person.email} onChange={handleEmailChange} /> </label> <p> {person.firstName}{' '} {person.lastName}{' '} ({person.email}) </p> </> ); }
Na primer, ova linija mutira state iz prethodnog rendera:
person.firstName = e.target.value;
Pouzdan način da dobijete željeno ponašanje je da kreirate novi objekat i prosledite ga u setPerson
. Ali, ovde želite i da kopirate postojeće podatke u njega, jer se samo jedno polje promenilo:
setPerson({
firstName: e.target.value, // Novo ime iz input-a
lastName: person.lastName,
email: person.email
});
Možete koristiti ...
objektnu spread sintaksu kako ne biste morali da kopirate svako polje pojedinačno.
setPerson({
...person, // Kopiraj stara polja
firstName: e.target.value // Override-uj ovo jedno
});
Sada forma radi!
Primetite da niste deklarisali posebnu state promenljivu za svako input polje. Za velike forme, grupisanje svih podataka u objekat je veoma zgodno—dok god ga ažurirate kako treba!
import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com' }); function handleFirstNameChange(e) { setPerson({ ...person, firstName: e.target.value }); } function handleLastNameChange(e) { setPerson({ ...person, lastName: e.target.value }); } function handleEmailChange(e) { setPerson({ ...person, email: e.target.value }); } return ( <> <label> Ime: <input value={person.firstName} onChange={handleFirstNameChange} /> </label> <label> Prezime: <input value={person.lastName} onChange={handleLastNameChange} /> </label> <label> Email: <input value={person.email} onChange={handleEmailChange} /> </label> <p> {person.firstName}{' '} {person.lastName}{' '} ({person.email}) </p> </> ); }
Primetite da je ...
spread sintaksa “plitka”—kopira polja samo na prvom nivou dubine. Ovo je čini brzom, ali, takođe znači da ako želite ažurirati ugnježdeno polje, moraćete je koristiti više od jednom.
Deep Dive
Možete koristiti [
i ]
zagrade unutar definicije objekta da specificirate polje sa dinamičkim imenom. Ovo je isti primer, ali sa jednim event handler-om umesto tri različita:
import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com' }); function handleChange(e) { setPerson({ ...person, [e.target.name]: e.target.value }); } return ( <> <label> Ime: <input name="firstName" value={person.firstName} onChange={handleChange} /> </label> <label> Prezime: <input name="lastName" value={person.lastName} onChange={handleChange} /> </label> <label> Email: <input name="email" value={person.email} onChange={handleChange} /> </label> <p> {person.firstName}{' '} {person.lastName}{' '} ({person.email}) </p> </> ); }
Ovde, e.target.name
predstavlja name
polje zadato u <input>
DOM elementu.
Ažuriranje ugnježdenog objekta
Pogledajte ovu strukturu ugnježdenog objekta:
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
Ako želite ažurirati person.artwork.city
, jasno je kako to uraditi kroz mutaciju:
person.artwork.city = 'New Delhi';
Ali, u React-u, tretirate state kao da je immutable! Da biste promenili city
, prvo trebate napraviti novi artwork
objekat (popunjen podacima iz prethodnog), a onda napraviti novi person
objekat koji pokazuje na novi artwork
:
const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);
Ili, napisano u jednom pozivu funkcije:
setPerson({
...person, // Kopiraj ostala polja
artwork: { // ali zameni artwork
...person.artwork, // sa svim istim poljima
city: 'New Delhi' // ali sa gradom New Delhi!
}
});
Ovo postaje dugačko, ali radi dobro u mnogim slučajevima:
import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: 'https://i.imgur.com/Sd1AgUOm.jpg', } }); function handleNameChange(e) { setPerson({ ...person, name: e.target.value }); } function handleTitleChange(e) { setPerson({ ...person, artwork: { ...person.artwork, title: e.target.value } }); } function handleCityChange(e) { setPerson({ ...person, artwork: { ...person.artwork, city: e.target.value } }); } function handleImageChange(e) { setPerson({ ...person, artwork: { ...person.artwork, image: e.target.value } }); } return ( <> <label> Naziv: <input value={person.name} onChange={handleNameChange} /> </label> <label> Naslov: <input value={person.artwork.title} onChange={handleTitleChange} /> </label> <label> Grad: <input value={person.artwork.city} onChange={handleCityChange} /> </label> <label> Slika: <input value={person.artwork.image} onChange={handleImageChange} /> </label> <p> <i>{person.artwork.title}</i> {' napravio/la '} {person.name} <br /> (locirano u {person.artwork.city}) </p> <img src={person.artwork.image} alt={person.artwork.title} /> </> ); }
Deep Dive
Ovaj objekat u kodu deluje kao “ugnježden”:
let obj = {
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
};
Međutim, “ugnježdavanje” je netačan način za razmišljanje o tome kako se objekti ponašaju. Kad se kod izvršava, ne postoji nešto što se naziva “ugnježden” objekat. Zapravo gledate u dva različita objekta:
let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};
let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1
};
Objekat obj1
nije “unutar” obj2
. Na primer, obj3
bi mogao da “pokazuje” na obj1
takođe:
let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};
let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1
};
let obj3 = {
name: 'Copycat',
artwork: obj1
};
Ako biste mutirali obj3.artwork.city
, to bi uticalo na obj2.artwork.city
i obj1.city
takođe. To se dešava zato što su obj3.artwork
, obj2.artwork
i obj1
isti objekat. Ovo je teško da se primeti kada mislite o objektima kao da su “ugnježdeni”. Umesto toga, to su odvojeni objekti koji “pokazuju” jedni na druge preko svojih polja.
Pisanje koncizne logike ažuriranja sa Immer-om
Ako je vaš state duboko ugnježden, možete razmisliti da ga flatten-ujete. Ali, ako ne želite promeniti strukturu vašeg state-a, možda biste voleli prečicu za ugnježdene spread-ove. Immer je popularna biblioteka koja omogućava pisanje zgodne sintakse za mutiranje i brine o pravljenju kopija za vas. Sa Immer-om, kod koji pišete deluje kao da “krši pravila” i mutira objekat:
updatePerson(draft => {
draft.artwork.city = 'Lagos';
});
Ali, za razliku od obične mutacije, ne menja prethodni state!
Deep Dive
draft
, promenljiva koju Immer uvodi, je poseban objekat koji se naziva Proxy, koji “beleži” šta radite sa njim. To je razlog zašto ga slobodno možete mutirati kako želite! Ispod haube, Immer prepoznaje koji delovi draft
-a su promenjeni i proizvodi potpuno novi objekat koji sadrži vaše promene.
Da biste isprobali Immer:
- Pokrenite
npm install use-immer
da dodate Immer kao zavisnost - Onda, zamenite
import { useState } from 'react'
saimport { useImmer } from 'use-immer'
Ovo je primer od gore konvertovan u 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": {} }
Primetite kako su event handler-i postali dosta koncizniji. Možete mešati useState
i useImmer
u jednoj komponenti koliko god želite. Immer je sjajan način za pisanje konciznih handler-a za ažuriranje, posebno ako imate ugnježdeni state, a kopiranje objekata vodi ka dupliranju koda.
Deep Dive
Postoji par razloga:
- Debug-ovanje: Ako koristite
console.log
i ne mutirate state, stari logovi neće biti pretrpani sa novijim izmenama state-a. Tako da možete jasno videti kako se state promenio između rendera. - Optimizacije: Uobičajene React-ove strategije optimizacije se oslanjaju na preskakanje posla ako su prethodni props-i ili state isti kao i novi. Ako nikada ne mutirate state, veoma se brzo proveri da li je bilo promena. Ako je
prevObj === obj
, možete biti sigurni da se ništa unutar njih nije promenilo. - Nove funkcionalnosti: Nove React funkcionalnosti koje pravimo se zasnivaju na tome da se state tretira kao snapshot. Ako mutirate prethodne verzije state-a, to vas može sprečiti da koristite nove funkcionalnosti.
- Promene zahteva: Neke funkcionalnosti, poput Undo/Redo, prikaza istorije promena ili dopuštanja korisniku da povrati formu na prethodne vrednosti, su jednostavnije kada ništa nije mutirano. Zato što možete čuvati prethodne kopije state-a u memoriji i koristiti ih kad vam je potrebno. Ako započnete sa pristupom mutacija, biće teže kasnije dodati ovakve funkcionalnosti.
- Jednostavnija implementacija: React ne treba da radi ništa specijalno sa vašim objektima, jer se ne oslanja na mutaciju. Ne treba da otima njihova polja, da ih obmotava u Proxy-je ili da radi druge stvari tokom inicijalizacije kao što to mnoga “reaktivna” rešenja rade. Baš zbog ovoga vam React dopušta da stavite bilo kakav objekat u state—nebitno koliko velik—bez dodatnih problema sa performansama i ispravnošću.
U praksi, često ćete moći da “se izvučete” sa mutiranjem state-a u React-u, ali vam ozbiljno savetujemo da to ne radite kako biste mogli da koristite nove React funkcionalnosti napravljene sa ovim pristupom na umu. Budući saradnici, a možda i vi u budućnosti, će vam biti zahvalni!
Recap
- Tretirajte svaki state u React-u kao immutable.
- Kada držite objekte u state-u, njihovim mutiranjem nećete pokrenuti rendere i izmenićete state u “snapshot-ovima” prethodnog rendera.
- Umesto mutiranja objekta, kreirajte novu verziju i pokrenite ponovni render postavljanjem state-a na tu novu verziju.
- Možete koristiti
{...obj, something: 'newValue'}
objektnu spread sintaksu za kreiranje kopija objekata. - Spread sintaksa je plitka: kopira samo jedan nivo u dubinu.
- Za ažuriranje ugnježdenog objekta morate kreirati kopije sve do mesta koji ažurirate.
- Za smanjivanje ponavljajućeg koda tokom kopiranja, koristite Immer.
Izazov 1 od 3: Popraviti netačna ažuriranja state-a
Ova forma ima par bug-ova. Kliknite dugme koje povećava rezultat par puta. Primetite da se ne povećava. Onda, promenite ime i primetite da se rezultat “uskladio” sa vašim izmenama. Konačno, promenite prezime i primetite da je rezultat potpuno nestao.
Vaš zadatak je da popravite ove bug-ove. Kad ih popravite, objasnite zašto se svaki od njih desio.
import { useState } from 'react'; export default function Scoreboard() { const [player, setPlayer] = useState({ firstName: 'Ranjani', lastName: 'Shettar', score: 10, }); function handlePlusClick() { player.score++; } function handleFirstNameChange(e) { setPlayer({ ...player, firstName: e.target.value, }); } function handleLastNameChange(e) { setPlayer({ lastName: e.target.value }); } return ( <> <label> Rezultat: <b>{player.score}</b> {' '} <button onClick={handlePlusClick}> +1 </button> </label> <label> Ime: <input value={player.firstName} onChange={handleFirstNameChange} /> </label> <label> Prezime: <input value={player.lastName} onChange={handleLastNameChange} /> </label> </> ); }