Чаму функцыя push() заставляе інтэрфейс React стаціонарным, і як новыя кансэкты гэта выправляюць
З'ясавайце, чаму зміна масэв у useState ніколі не спрыяе паўтарнаму атрыбутаванню, як працюе перагляд кансэртаў у React, і калі трэба вжываць копіі з распрасованнем і функцыональныя апдэйты.
Крыніцы: React Survival Guide, Part 2: Don’t Mutate! Why `.push()` is Killing Your UI
Мало які багі ў React так супяжнююць, як карточка інтэрфейсу, яка адмовляецца апдэйтавацца, паколькі консоль стверджуе, што даныя правильныя. Не існуе ні памылак, ні апавяшэнняў — толькі экран застаецца з старымі значэннямі. У пераважнае большасць случаў прычыной ёст трансфармація стану: змена масівы аб’екта на месцы заместо яе замены. У гэтым артыкуле дакладна пояснюецца, чаму React ігнаруе змены на месцы, паказана способнасць выправлення ў аднай лініі, а таксама рассказваецца пра функцыональную форму апдэйта, якая патрэбна, калі апдэйты накапліваюцца быстра.
Баг: даныя змінююцца, а экран — ні
Уявіце сабе маленькую корзінку пакупак, список задач або табелю корыстувальнікаў. Апошняя задача — дадаць элемент у масаву, якая зберагаецца ў useState, а першая спроба выглядае як звычны JavaScript.
const [cart, setCart] = useState(['Coffee']);
const addItem = (newItem) => {
// It's just JavaScript, right?
cart.push(newItem);
console.log("Added! Cart is now:", cart);
};
Пасля нажатыя на „Дадзіць у кошачку“ лог паказвае ['Coffee', 'Tea'], таму даныя выглядаюць правільна. Аднак у адразе кошачкі все роўна паказваецца толькі „Coffee“. Інтэрфейс тады паказвае корыстнікам інформацыю, якая вучора ўжо не ўсунутая, і гэта горш за злам, калі ўсё можна не пазначыць, пакуль кліент гэта не з’явіць. У такіх ситуацыях разработчыкі часта витрачаюць гадзину на перапраналік циклаў і логіки адразавання, якія ніколі не былі прычыной проблемы.
Заўважыце, што гэты хендлер абоўсюды не вызывае setCart. Нічога не паведаміла React пра тое, што ўсё-такі адбылася, таму у яе не было прычыны адразаваць. Болей тонкі варіянт бага дадае setCart(cart) пасля push(), а экран такі ж не змянюецца. У наступным раздзеле будзе пасвячана адпаведнае поясненне.
React парабягаеся на рэферэнсах, а не на ўмесце
Ключовая ідея, яку трэба усвядоміць, простая: React стежыць за ідэнтычнасцю, а не за контентам.
Методы такія як .push(), .pop(), .splice() і .unshift(), або прызначэння на кшталт user.name = 'Alice' ўважаюцца мутацыямі. Яны прасягають JavaScript змяніць значэнне, якое зберагаецца ў вядомым месцы ў памяці. JavaScript робіць гэта негайна, таму console.log(cart) паказвае новы элемент. Разам з тым React не стежыць за гэтай памяцю. Яго модель апдэйта прыпускае, што стан є незменным.
Калі вы вызываеце функцыю для змены стану, React практыкуеўа порэванае новага значэння з ныякім за дапамою Object.is. Для масэў і об’ектаў гэта ёсць порэванае па рэферэнсу: ён проста пераканваецца, чы не яўляецца гэты той самы об’ект, што і раней, а не чы змениліся яго элементы. Пасля вызыву push() маса, яку вы вяртаеце, — гэта той самы об’ект, які вже зберагаецца у React. Пры рэзультате „незменена“ React прыпінае перзапісваў і интерфейс продовжвае паказваць последнюю версію.
Гэтае простае перакананне є намераваным. Порэванае па рэферэнсу займае сталякі час, незалежна ад габаратаў стану, тады як глэбокае перакананне кожнага вярстанога значэння пасля кожной змены было б дужа дорогім. Компрасам — вам неабходна падаць новы рэферэнс, каб сигналізаваць пра змяну.
Рашэнне: скопіюйце, зменіце копію, задаце копію
Незменнае становісць здаецца болей выказнай, чым ёсць на самай працэ. Ёк хоча б аднакоўваць масів чы рэкурсію ў стане, трэба заліць первасны варыянт без змян, створыць новы, які включае змяну, і перадаць його функцыі для задання стану. У сучасным JavaScript аператар распространення (...) — гэта найшырэйшы спосаб так чыніць.
Верныяе выкананне функцыі addItem стварае новы масів, які складаецца з усіх паказаных раней элементаў, а пасля — з новага элемента.
const addItem = (newItem) => {
// The right way (creating a new identity):
setCart([...cart, newItem]); // Create a new array, add newItem at the end.
};
Тепер функцыя для задання стану прыме іншую рэферэнцю, перакананне за дапамою Object.is не будзе успэшным, і React перыскаладаў стан з даданым элементам. Той жа прынцып прабывае і для іншых операцый:
- Адчысленне:
cart.filter(item => item !== target)заместоsplice(). - Замена:
cart.map(item => (item.id === id ? { ...item, qty: item.qty + 1 } : item)).
setUser({ ...user, name: 'Alice' }) замест на прызначэнне значэння практычным атрыбутам.[...cart].sort(), але іспользуйце методы toSorted() і toReversed(), якія не змінююць масэвуючы, там, дзе яны падтрымляюцца вашымі браузерамі.Памятайце, што методы распрасовання ствараюць копію толькі на аднам роўні. Якщо стан содержыць вярстаўленыя об’екты, кожны роўнь, які вы зменяеце, патрэбуе сваёй сабскопіі. Чысткага аналізу мутацый касоў у React, укладаючы вярстаўленыя структуры, можна знайсці ў стацыі пра адварот мовчазных багоў стану з-за мутацый касоў у JavaScript.
Што дае вам незменнасць
Гэта не проста правіла, якія накладае React; гэта таксама дае код, які легчэй разумець.
- Чыстыя пункты апдэйта. Кожныя змены стану праходзіць через функцыю-зменяйшык, таму ясна, якія функцыі мەнююць стан і дзе.
- Лепшая дэбаггінг. Пакалькі кожны апдэйт стварае ўнікальную значэнне, React DevTools і іншыя інструменты можаюць паказаць стан да і пасля кожной змены.
- Няма супакрытых парадоксальных наследків. Калі даныя ніколі не мянююцца на месцы, баг у адной складовай не можа таямніча зіпсаваць масэвую структуру або об’ект, які іншая частка прыложэння ўсё ўжо чытае.
Функцыйнае апдэйтаванне для шырокага або перакрытых змян
Другая, сувязаная проблема выклікаецца пад частыя апдэйты, такія як швыядзе павтаральныя клікі або колькасць асінхронных адпаведзей, якія прыходзяць падалёку адзін ад другога, можліва, не па порядку. Кожны працоўнік захопляе значэнне стану з таго рендара, у яком быў створаны. Якщо два апдэйты выконваюцца перад паўторным рендаром, оба можу прыступіць да работы з там самым застарэлым кадрам, і адна змянаОвершвае іншую. Гэта ёсць проблема застарэлага закрыцця, і яна безпасередні ўзаемадзейнае з тым, як вы ствараеце новы стан.
Рэшэння — перадаць функцыю ў сетар. React вызывае яе з найновейшым чакаючым станам, тады кожны апдэйт будуе на рэзультате пярэдньаго, а не на захопленым значэнні. У прыкладе нижчэй дадаўаецца група створаных элементаў. Ён чытае prevCart.length з аргумента, каб прызначыць іх номеры, а потым вяртае абсалютна новы масіў, які з’едначае пярэднія элементы і новыя.
// A very fast counter:
const addManyItems = (count) => {
// Good: Uses spread to be immutable
// Better: Uses functional update for consistency
setCart(prevCart => {
// We are generating new array content:
const newItems = Array.from({ length: count }, (_, i) => `Item ${prevCart.length + i + 1}`);
// Spread operator for the final, clean, new identity:
return [...prevCart, ...newItems];
});
};
Ёсць два факторы, які працуюць разам. Функцыя spread все ўсё стварае новую рэферэнцю, і самэ гэта змушвае React перыскаляць интерфейс. Функцыя-апдэйтар гарантуе, што базовы масіў є найсвежэйшым, і гэта запобегае прыпыненню апдэйтав. Хорашая прычына — вжываць функцыйнае выражэнне кожны раз, калі наступны стан вычысляецца з пярэдньага.
Узагальненне
- Мутацыя змінюе даны, але не рэферэнцю, таму пераказка
Object.isу React выявляе, што нічога не зменілася, і прыпіняе перыскаляванне. - Калі інтерфейс адстае ад даных, спачатку шукайце вызывы
.push(),.unshift(),.splice()абоstate.property = valueу стане. - Створыце новы масіў або об’ект за дапамойкай spread,
mapабоfilter, і скопіюйце кожны внутрані рэвень, які вы зменяеце.
Дыктавка зазвычай вырабляецца за дапамою адной лініі коду, і гэта ўсё, што розлічва інтэрфейс, якому можа даваць паверынне корыстувач, ад таго, які таямна паказвае застарэлую інфармацыю.