Чтобы изменить глубоко вложенные объекты / переменные в состоянии React, обычно используются три метода: vanilla JavaScript's Object.assign, immutability-helper и cloneDeepиз Lodash .
Для этого есть множество других менее популярных сторонних библиотек, но в этом ответе я расскажу только об этих трех вариантах. Кроме того, существуют некоторые дополнительные методы JavaScript, такие как распространение массива (см., Например, ответ @ mpen), но они не очень интуитивны, просты в использовании и способны обрабатывать все ситуации с манипуляциями с состоянием.
Как отмечалось, бесчисленное количество раз в топ-голосующих комментариях к ответам, авторы которых предлагают прямую мутацию состояния: просто не делайте этого . Это вездесущий анти-паттерн React, который неизбежно приведет к нежелательным последствиям. Учитесь правильно.
Давайте сравним три широко используемых метода.
Учитывая это состояние объекта структуры:
state = {
outer: {
inner: 'initial value'
}
}
Вы можете использовать следующие методы для обновления значения самого внутреннего innerполя, не затрагивая остальную часть состояния.
1. Ванильный JavaScript Object.assign
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()setter для замены оригинала outerв состоянии вновь созданным newOuterобъектом (изменится только значение, имя свойства outerне будет).
Теперь представьте, что у нас есть более глубокое состояние, как state = { outer: { inner: { innerMost: 'initial value' } } }. Мы могли бы попытаться создать newOuterобъект и заполнить его outerсодержимым из состояния, но Object.assignне сможем скопировать innerMostзначение в этот вновь созданный newOuterобъект, поскольку innerMostоно вложено слишком глубоко.
Вы все еще можете копировать inner, как в примере выше, но, поскольку теперь это объект, а не примитив, ссылка from 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>
CloneDeep от Lodash более прост в использовании. Он выполняет глубокое клонирование , поэтому это надежный вариант, если у вас достаточно сложное состояние с многоуровневыми объектами или массивами внутри. Просто 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.
если ты манипулируете огромным и / или сложным состоянием , Lodash's cloneDeep- мудрый выбор.
Если тебе надо расширенные возможности , то есть, если ваша структура состояний сложна и вам нужно выполнять с ней все виды операций, попробуйте immutability-helper, это очень продвинутый инструмент, который можно использовать для манипулирования состояниями.
... или ты действительно должны сделать это вообще?
Если вы храните сложные данные в состоянии React, возможно, сейчас самое время подумать о других способах их обработки. Задание объектов сложных состояний прямо в компонентах React не является простой операцией, и я настоятельно рекомендую подумать о разных подходах.
Скорее всего, вам лучше не хранить свои сложные данные в хранилище Redux, устанавливать их там с помощью редукторов и / или саг и обращаться к ним с помощью селекторов.