Для того, щоб змінити глибоко вкладені об'єкти / змінні в реактив стану, як правило , використовується три методи: ваніль в 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, встановлювати їх там, використовуючи редуктори та / або саги, і отримувати доступ до них за допомогою селекторів.