Към съдържанието
wannacode

Обновяване на обекти и масиви

Накратко: Обект или масив в състоянието на React се обновява с ново копие (spread, map, filter), защото React сравнява референции и не вижда промяна на стария.

Проблем

„Махни“ не маха

Кошницата има три реда, всеки с бутон „Махни“. Натисни „Махни“ на хляба.

React

Конзолата казва редове: 2. На екрана са трите реда и старата сума. Данните са сменени, а екранът не ги следва, и грешка няма.

remove маха реда със splice и вика setCart(cart). Новата стойност е подадена. Защо React не рисува?

Идеята

Как библиотеката вижда промяната

С число е ясно: 1 и 2 са различни. Масивът и обектът обаче са референции: променливата сочи обект в паметта. Има два въпроса. Дали променливата сочи друг обект? И дали съдържанието на същия обект е сменено? Всяка библиотека пита по свой начин, и тук разликата се вижда в кода.

React пита само първия въпрос. При setCart(next) той сравнява старото и новото състояние с Object.is. За масив това е „същият обект ли е“, а не „същото съдържание ли има“.

splice сменя съдържанието на същия масив. setCart(cart) подава същата референция, Object.is казва „същото“ и React пропуска рисуването. Затова в React масивът и обектите в състоянието не се сменят на място. Прави се нов масив и React вижда нова референция. Това се казва непроменимо обновяване (immutable update).

Виж го

Виж: същият масив или нов

const [cart, setCart] = useState([bread]);
function addMilk() {
cart.push(milk);
setCart(cart);
}
function addCheese() {
setCart([...cart, cheese]);
}
Стъпка по стъпка, с думи
  1. cart сочи масив с един ред. React пази тази референция като състоянието на компонента, а екранът показва хляба.
  2. cart.push(milk) добавя ред в същия масив. Съдържанието е друго, но обектът е същият.
  3. setCart(cart) подава същата референция. React сравнява старата и новата с Object.is: същият обект.
  4. Същото значи „нищо ново“: React пропуска рисуването. Млякото е в масива, а не на екрана.
  5. [...cart, cheese] прави нов масив: копие на редовете и сиренето.
  6. setCart с новия масив: Object.is казва „различни“ и React рисува наново.
  7. Сега на екрана са трите реда. И млякото, което досега беше скрито в стария масив.
  8. Правилото в React: не сменяй масива в състоянието, а му дай нов.
Млякото е в масива още от push, но на екрана се появява чак със сиренето. Кодът с мутация не просто не работи. Той крие промяна, която излиза по-късно, при съвсем друго действие.

Идеята

Нов масив, нов обект

Кошницата има три действия: добави ред, смени броя на един ред, махни ред.

// добави: нов масив със стария и новия ред
setCart([...cart, { ...product, qty: 1 }]);
// смени един ред: нов масив, сменият ред е нов обект
setCart(cart.map((line) => (line.id === id ? { ...line, qty: line.qty + 1 } : line)));
// махни: нов масив без реда
setCart(cart.filter((line) => line.id !== id));
  • [...cart, x] е нов масив: старите елементи и x накрая.
  • { ...line, qty: 5 } е нов обект: полетата на line, а qty е сменено.
  • map и filter винаги връщат нов масив. Затова са основните инструменти тук.

Пробвай

Поправи „Махни“

Това е кошницата от началото. Поправи remove така, че редът да изчезва и сумата да се смята наново. Проверката маха хляба, после сиренето.

React

Пробвай

Намери грешката

„Махни“ не маха. Кой ред е виновен?

Избери реда с грешката.

Идеята

Един ред от масива

Купувачът натиска „+“ на хляба. Трябва да се смени qty на един обект, а той е в масив.

cart.find(...).qty++ сменя обекта на място: той е същият обект, който React пази като старо състояние. Масивът е същият, затова React не рисува. Числото не изчезва: то излиза на екрана при следващото рисуване по друга причина и броят „скача“.

Внимавай и с копията. [...cart] е нов масив, но редовете в него са същите обекти. Това е плитко копие: копира се само първото ниво. copy[0].qty++ сменя и реда в старото състояние. Сменяният ред трябва да е нов обект: { ...line, qty: line.qty + 1 }. Редовете, които не се сменят, остават същите обекти, и това е наред.

По-точно, за любопитните

Object.is. Като ===, с две разлики: Object.is(NaN, NaN) е true, а Object.is(0, -0) е false. За масиви и обекти и двете сравняват референции.

Вложени обекти. Всяко ниво от пътя до промяната е ново: { ...order, address: { ...order.address, city: 'Варна' } }. Нивата встрани остават същите.

structuredClone(cart) прави дълбоко копие: и масивът, и всеки обект в него. Работи, но копира всичко при всяка промяна, а функции не може да копира. За един ред map и ... са по-евтини.

Immer. Библиотеката Immer позволява да пишеш като с мутация (draft[0].qty++), а тя прави новите обекти вместо теб. В пясъчника я няма.

Работи и във Vue. Новият масив е правилен и във Vue: cart.value = cart.value.filter(...). Обратното не важи: мутацията, нормална във Vue, в React не се вижда.

Пробвай

„+“ и „−“

Направи more и less. „+“ вдига броя на реда с 1, а „−“ го сваля с 1. Когато броят е 1, „−“ маха реда. Сумата следва броя.

React

Упражнение

Каталог и кошница

Каталогът има четири продукта с бутон „Добави“ (името за екранния четец: „Добави Бял хляб“). Направи add и remove. Първият клик на „Добави“ прави ред с брой 1, а следващите вдигат броя на същия ред, вместо да правят нов. „Махни“ маха реда. Когато кошницата е празна, пише „Кошницата е празна“.

React

Тест

Провери се

1. cart.push(line); setCart(cart); Защо екранът не се сменя?
2. Кое вдига броя на един ред правилно?
3. const next = [...cart]; next[0].qty++; setCart(next); Какво не е наред?
4. Как се маха ред без да се сменя масивът на състоянието?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·