Чтобы изменить глубоко вложенные объекты / переменные в состоянии 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, устанавливать их там с помощью редукторов и / или саг и обращаться к ним с помощью селекторов.