Для того, щоб змінити глибоко вкладені об'єкти / змінні в реактив стану, як правило , використовується три методи: ваніль в JavaScript Object.assign
, незмінність-помічник і cloneDeep
від Lodash .
Існує також безліч інших менш популярних сторонніх прихильників для досягнення цього, але в цій відповіді я висвітлю саме ці три варіанти. Також існують деякі додаткові методи ванільного JavaScript, як-от розсип масиву (див. Наприклад відповідь @ mpen), але вони не дуже інтуїтивно зрозумілі, прості у використанні та здатні впоратися з усіма ситуаціями маніпулювання державою.
Як неодноразово вказувалося, вгорі голосували коментарі до відповідей, автори яких пропонують пряму мутацію стану: просто не робіть цього . Це повсюдна антиреакція React, яка неминуче призведе до небажаних наслідків. Дізнайтеся правильний шлях.
Порівняємо три широко використовувані методи.
З огляду на таку структуру об'єкта стану:
state = {
outer: {
inner: 'initial value'
}
}
Ви можете використовувати наступні методи для оновлення значення самого внутрішнього inner
поля, не впливаючи на решту стану.
1. Object.assign ванільного JavaScript
const App = () => {
const [outer, setOuter] = React.useState({ inner: 'initial value' })
React.useEffect(() => {
console.log('Before the shallow copying:', outer.inner) // initial value
const newOuter = Object.assign({}, outer, { inner: 'updated value' })
console.log('After the shallow copy is taken, the value in the state is still:', outer.inner) // initial value
setOuter(newOuter)
}, [])
console.log('In render:', outer.inner)
return (
<section>Inner property: <i>{outer.inner}</i></section>
)
}
ReactDOM.render(
<App />,
document.getElementById('react')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
<main id="react"></main>
Майте на увазі, що Object.assign не буде виконувати глибоке клонування , оскільки він копіює лише значення властивостей , і тому те, що він робить, називається дрібною копією (див. Коментарі).
Щоб це працювало, нам слід лише маніпулювати властивостями примітивних типів ( outer.inner
), тобто рядків, чисел, булів.
У цьому прикладі ми створюємо нову константу ( const newOuter...
), використовуючи Object.assign
, яка створює порожній об’єкт ( {}
), копіює в нього outer
об'єкт ( { inner: 'initial value' }
), а потім копіює інший об’єкт { inner: 'updated value' }
над ним.
Таким чином, врешті-решт, новостворена newOuter
константа утримуватиме значення з { inner: 'updated value' }
моменту inner
переоцінки властивості. Це newOuter
абсолютно новий об'єкт, який не пов'язаний з об'єктом у стані, тому він може бути вимкнено, якщо потрібно, і стан залишиться таким самим і не зміниться, поки не буде запущена команда оновлення.
Остання частина полягає у використанні setOuter()
сеттера для заміни оригіналу outer
в стані новоствореним newOuter
об'єктом (зміниться лише значення, ім'я властивості outer
не буде).
А тепер уявімо, що у нас є більш глибокий стан state = { outer: { inner: { innerMost: 'initial value' } } }
. Ми можемо спробувати створити newOuter
об’єкт і заповнити його outer
вмістом у державі, але Object.assign
не зможемо скопіювати innerMost
значення цього новоствореного newOuter
об’єкта, оскільки innerMost
він вкладений занадто глибоко.
Ви все ще можете скопіювати inner
, як у наведеному вище прикладі, але оскільки це тепер об'єкт, а не примітив, посилання від newOuter.inner
буде скопійовано outer.inner
натомість, це означає, що ми в кінцевому підсумку з локальним newOuter
об’єктом, безпосередньо прив'язаним до об'єкта в державі .
Це означає, що в цьому випадку мутації локально створених newOuter.inner
безпосередньо впливатимуть на outer.inner
об'єкт (у стані), оскільки вони насправді стали тим самим (в пам'яті комп'ютера).
Object.assign
тому працюватиме лише у тому випадку, якщо у вас є відносно проста однорівнева глибока структура стану з найпотаємнішими елементами, що містять значення примітивного типу.
Якщо у вас є більш глибокі об'єкти (2-й рівень або більше), які слід оновити, не використовуйте Object.assign
. Ви ризикуєте мутувати стан безпосередньо.
2. Клон ЛодашаГлубокий
const App = () => {
const [outer, setOuter] = React.useState({ inner: 'initial value' })
React.useEffect(() => {
console.log('Before the deep cloning:', outer.inner) // initial value
const newOuter = _.cloneDeep(outer) // cloneDeep() is coming from the Lodash lib
newOuter.inner = 'updated value'
console.log('After the deeply cloned object is modified, the value in the state is still:', outer.inner) // initial value
setOuter(newOuter)
}, [])
console.log('In render:', outer.inner)
return (
<section>Inner property: <i>{outer.inner}</i></section>
)
}
ReactDOM.render(
<App />,
document.getElementById('react')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
<main id="react"></main>
ClonasDeep клонада Лодаша - простіший у використанні. Він виконує глибоке клонування , тому він є надійним варіантом, якщо у вас досить складний стан з багаторівневими об'єктами або масивами всередині. Просто cloneDeep()
властивість державного рівня вищого рівня, мутуйте клоновану частину будь-яким способом, і setOuter()
поверніть її до держави.
3. незмінність-помічник
const App = () => {
const [outer, setOuter] = React.useState({ inner: 'initial value' })
React.useEffect(() => {
const update = immutabilityHelper
console.log('Before the deep cloning and updating:', outer.inner) // initial value
const newOuter = update(outer, { inner: { $set: 'updated value' } })
console.log('After the cloning and updating, the value in the state is still:', outer.inner) // initial value
setOuter(newOuter)
}, [])
console.log('In render:', outer.inner)
return (
<section>Inner property: <i>{outer.inner}</i></section>
)
}
ReactDOM.render(
<App />,
document.getElementById('react')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
<script src="https://wzrd.in/standalone/immutability-helper@3.0.0"></script>
<main id="react"></main>
immutability-helper
бере його на зовсім новий рівень, і прохолодна річ про це є те , що він може не тільки $set
значення для державних елементів, але і $push
, $splice
, $merge
( і т.д.) їх. Ось список доступних команд .
Бічні нотатки
Знову ж таки, майте на увазі, що setOuter
змінює лише властивості першого рівня об'єкта стану ( outer
у цих прикладах), а не глибоко вкладені ( outer.inner
). Якби він поводився по-іншому, цього питання не існувало б.
Який з них підходить для вашого проекту?
Якщо ви не хочете або не можете використовувати зовнішні залежності та маєте просту структуру стану , дотримуйтесь Object.assign
.
Якщо ви маніпулюєте величезним та / або складним станом , Лодаш cloneDeep
- це мудрий вибір.
Якщо вам потрібні розширені можливості , тобто, якщо ваша державна структура є складною і вам потрібно виконувати всі види операцій над нею, спробуйте immutability-helper
, це дуже просунутий інструмент, який можна використовувати для маніпулювання станом.
... або, чи справді взагалі потрібно це робити?
Якщо ви тримаєте складні дані в стані React, можливо, це вдалий час, щоб подумати про інші способи поводження з ними. Встановлення складних об'єктів стану прямо в компонентах React не є простою операцією, і я настійно пропоную подумати про різні підходи.
Швидше за все, вам краще не зберігати складні дані в магазині Redux, встановлювати їх там, використовуючи редуктори та / або саги, і отримувати доступ до них за допомогою селекторів.