this.setState не объединяет состояния, как я ожидал

160

У меня есть следующее состояние:

this.setState({ selected: { id: 1, name: 'Foobar' } });  

Затем я обновляю состояние:

this.setState({ selected: { name: 'Barfoo' }});

Поскольку setStateпредполагается слияние, я ожидаю, что это будет:

{ selected: { id: 1, name: 'Barfoo' } }; 

Но вместо этого он ест идентификатор и состояние:

{ selected: { name: 'Barfoo' } }; 

Это ожидаемое поведение и каково решение для обновления только одного свойства вложенного объекта состояния?

Pickels
источник

Ответы:

143

Я думаю, setState()что не делает рекурсивное слияние.

Вы можете использовать значение текущего состояния this.state.selectedдля создания нового состояния, а затем вызвать его setState():

var newSelected = _.extend({}, this.state.selected);
newSelected.name = 'Barfoo';
this.setState({ selected: newSelected });

Я использовал функцию _.extend()function (из библиотеки underscore.js), чтобы предотвратить изменение существующей selectedчасти состояния, создав ее поверхностную копию.

Другое решение - написать setStateRecursively()рекурсивное слияние в новом состоянии и затем вызвать replaceState()его:

setStateRecursively: function(stateUpdate, callback) {
  var newState = mergeStateRecursively(this.state, stateUpdate);
  this.replaceState(newState, callback);
}
andreypopp
источник
7
Надежно ли это работает, если вы делаете это более одного раза или есть вероятность того, что реакция отразится в очереди на вызовы replaceState, а последний выиграет?
edoloughlin
111
Для людей, которые предпочитают использовать удлинение и которые также используют babel, вы можете сделать это, this.setState({ selected: { ...this.state.selected, name: 'barfoo' } })что переводится наthis.setState({ selected: _extends({}, this.state.selected, { name: 'barfoo' }) });
Pickels
8
@Pickels это здорово, приятно идиоматично для React и не требует underscore/ lodash. Выделите это в свой ответ, пожалуйста.
ericsoco
14
this.state не обязательно в актуальном состоянии . Вы можете получить неожиданные результаты, используя метод расширения. Передача функции обновления setStateявляется правильным решением.
Стром
1
@ EdwardD'Souza Это библиотека Лодаш. Он обычно вызывается как подчеркивание, так же, как jQuery обычно вызывается как знак доллара. (Итак, это _.extend ().)
mjk
96

Помощники неизменности были недавно добавлены в React.addons, поэтому теперь вы можете делать что-то вроде:

var newState = React.addons.update(this.state, {
  selected: {
    name: { $set: 'Barfoo' }
  }
});
this.setState(newState);

Документация помощников по неизменности .

user3874611
источник
7
обновление устарело. facebook.github.io/react/docs/update.html
thedanotto
3
@thedanotto Похоже, что React.addons.update()метод устарел, но библиотека замены github.com/kolodny/immutability-helper содержит эквивалентную update()функцию.
Bungle
37

Поскольку многие ответы используют текущее состояние в качестве основы для объединения новых данных, я хотел бы отметить, что это может сломаться. Изменения состояния ставятся в очередь и не сразу изменяют объект состояния компонента. Поэтому ссылка на данные состояния до обработки очереди даст вам устаревшие данные, которые не отражают ожидающие изменения, сделанные вами в setState. Из документов:

setState () не изменяет немедленно this.state, но создает ожидающий переход в состояние. Доступ к this.state после вызова этого метода может потенциально вернуть существующее значение.

Это означает, что использование «текущего» состояния в качестве ссылки в последующих вызовах setState ненадежно. Например:

  1. Первый вызов setState, стоящий в очереди изменения состояния объекта
  2. Второй вызов setState. Ваше состояние использует вложенные объекты, поэтому вы хотите выполнить слияние. Перед вызовом setState вы получаете объект текущего состояния. Этот объект не отражает вышеуказанные изменения, внесенные при первом вызове setState, потому что это все еще исходное состояние, которое теперь следует считать «устаревшим».
  3. Выполнить слияние. Результатом является исходное «устаревшее» состояние плюс новые данные, которые вы только что установили, изменения от начального вызова setState не отражаются. Ваш вызов setState ставит в очередь это второе изменение.
  4. Реагирует на очередь процессов. Сначала обрабатывается вызов setState, обновляется состояние. Второй вызов setState обрабатывается, обновляется состояние. Второй объект setState теперь заменил первый, и поскольку данные, которые вы имели при выполнении этого вызова, устарели, измененные устаревшие данные этого второго вызова засоряли изменения, сделанные в первом вызове, которые потерялись.
  5. Когда очередь пуста, React определяет, следует ли визуализировать и т. Д. На этом этапе вы будете визуализировать изменения, сделанные во втором вызове setState, и будет так, как если бы первый вызов setState никогда не происходил.

Если вам нужно использовать текущее состояние (например, для объединения данных во вложенный объект), setState альтернативно принимает функцию в качестве аргумента вместо объекта; функция вызывается после любых предыдущих обновлений состояния и передает состояние в качестве аргумента - так что это может быть использовано для внесения атомарных изменений, гарантирующих уважение предыдущих изменений.

bgannonpl
источник
5
Есть ли пример или несколько документов о том, как это сделать? afaik, никто не принимает это во внимание.
Josef.B
@ Денис Попробуйте мой ответ ниже.
Мартин Доусон
Я не вижу, где проблема. То, что вы описываете, произойдет, только если вы попытаетесь получить доступ к «новому» состоянию после вызова setState. Это совершенно другая проблема, которая не имеет ничего общего с вопросом ОП. Доступ к старому состоянию перед вызовом setState, чтобы вы могли создать новый объект состояния, никогда не будет проблемой, и фактически это именно то, что ожидает React.
nextgentech
@nextgentech «Доступ к старому состоянию перед вызовом setState, чтобы вы могли создать новый объект состояния, никогда не будет проблемой» - вы ошибаетесь. Мы говорим о состоянии перезаписи на основе this.state, которое может быть устаревшим (или, скорее, в ожидании обновления в очереди), когда вы обращаетесь к нему.
Madbreaks
1
@Madbreaks Хорошо, да, после перечитывания официальных документов я вижу, что указано, что вы должны использовать функциональную форму setState для надежного обновления состояния на основе предыдущего состояния. Тем не менее, я никогда не сталкивался с проблемой на сегодняшний день, если не пытался вызывать setState несколько раз в одной и той же функции. Спасибо за ответы, которые заставили меня перечитать спецификации.
nextgentech
10

Я не хотел устанавливать другую библиотеку, так что вот еще одно решение.

Вместо того:

this.setState({ selected: { name: 'Barfoo' }});

Сделайте это вместо этого:

var newSelected = Object.assign({}, this.state.selected);
newSelected.name = 'Barfoo';
this.setState({ selected: newSelected });

Или, благодаря @ icc97 в комментариях, еще более лаконично, но, возможно, менее читабельно:

this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });

Кроме того, чтобы быть ясным, этот ответ не нарушает ни одну из проблем, упомянутых @bgannonpl выше.

Райан Шиллингтон
источник
Upvote, проверьте. Просто интересно, почему бы не сделать это так? this.setState({ selected: Object.assign(this.state.selected, { name: "Barfoo" }));
Юстус
1
@JustusRomijn К сожалению, тогда вы будете изменять текущее состояние напрямую. Object.assign(a, b)принимает атрибуты b, вставляет / перезаписывает их, aа затем возвращает a. Реагировать на людей можно, если вы измените состояние напрямую.
Райан Шиллингтон
Именно. Просто отметьте, что это работает только для мелкого копирования / назначения.
Madbreaks
1
@JustusRomijn Вы можете сделать это на MDN правопреемника в одной строке , если вы добавите в {}качестве первого аргумента: this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });. Это не изменяет исходное состояние.
icc97
@ icc97 Отлично! Я добавил это к своему ответу.
Райан Шиллингтон
8

Сохранение предыдущего состояния на основе ответа @bgannonpl:

Пример Лодаша :

this.setState((previousState) => _.merge({}, previousState, { selected: { name: "Barfood"} }));

Чтобы убедиться, что все работает правильно, вы можете использовать функцию обратного вызова второго параметра:

this.setState((previousState) => _.merge({}, previousState, { selected: { name: "Barfood"} }), () => alert(this.state.selected));

Я использовал, mergeпотому чтоextend что иначе отбрасывает другие свойства.

Приведите пример неизменности :

import update from "react-addons-update";

this.setState((previousState) => update(previousState, {
    selected:
    { 
        name: {$set: "Barfood"}
    }
});
Мартин Доусон
источник
2

Мое решение для такой ситуации заключается в использовании, как указано в другом ответе, помощников по неизменности .

Поскольку установка состояния в глубину - обычная ситуация, я создал следующий миксин:

var SeStateInDepthMixin = {
   setStateInDepth: function(updatePath) {
       this.setState(React.addons.update(this.state, updatePath););
   }
};

Этот миксин входит в большинство моих компонентов, и я обычно не использую setState напрямую.

С этим миксином все, что вам нужно сделать для достижения желаемого эффекта, это вызвать функцию setStateinDepthследующим образом:

setStateInDepth({ selected: { name: { $set: 'Barfoo' }}})

Чтобы получить больше информации:

Дориан
источник
Извините за поздний ответ, но ваш пример Mixin кажется интересным. Однако, если у меня есть 2 вложенных состояния, например this.state.test.one и this.state.test.two. Верно ли, что только один из них будет обновлен, а другой будет удален? Когда я обновлю первый, если второй я в состоянии, он будет удален ...
mamruoc
hy @mamruoc, this.state.test.twoвсе еще будет там после того, как вы обновите this.state.test.oneс помощью setStateInDepth({test: {one: {$set: 'new-value'}}}). Я использую этот код во всех своих компонентах, чтобы обойти ограниченную setStateфункцию React .
Дориан
1
Я нашел решение. Я инициировал this.state.testкак массив. Перейдя на Объекты, решил это.
mamruoc
2

Я использую классы es6, и в конечном итоге у меня появилось несколько сложных объектов в верхнем состоянии, и я пытался сделать мой основной компонент более модульным, поэтому я создал простую оболочку класса, чтобы сохранить состояние верхнего компонента, но с учетом большей локальной логики. ,

Класс-оболочка принимает в качестве конструктора функцию, которая устанавливает свойство основного состояния компонента.

export default class StateWrapper {

    constructor(setState, initialProps = []) {
        this.setState = props => {
            this.state = {...this.state, ...props}
            setState(this.state)
        }
        this.props = initialProps
    }

    render() {
        return(<div>render() not defined</div>)
    }

    component = props => {
        this.props = {...this.props, ...props}
        return this.render()
    }
}

Затем для каждого сложного свойства в верхнем состоянии я создаю один класс StateWrapped. Здесь вы можете установить реквизиты по умолчанию в конструкторе, и они будут установлены при инициализации класса, вы можете обратиться к локальному состоянию для значений и установить локальное состояние, обратиться к локальным функциям и передать их по цепочке:

class WrappedFoo extends StateWrapper {

    constructor(...props) { 
        super(...props)
        this.state = {foo: "bar"}
    }

    render = () => <div onClick={this.props.onClick||this.onClick}>{this.state.foo}</div>

    onClick = () => this.setState({foo: "baz"})


}

Тогда моему компоненту верхнего уровня просто нужен конструктор, чтобы установить для каждого класса его свойство состояния верхнего уровня, простую визуализацию и любые функции, которые взаимодействуют между компонентами.

class TopComponent extends React.Component {

    constructor(...props) {
        super(...props)

        this.foo = new WrappedFoo(
            props => this.setState({
                fooProps: props
            }) 
        )

        this.foo2 = new WrappedFoo(
            props => this.setState({
                foo2Props: props
            }) 
        )

        this.state = {
            fooProps: this.foo.state,
            foo2Props: this.foo.state,
        }

    }

    render() {
        return(
            <div>
                <this.foo.component onClick={this.onClickFoo} />
                <this.foo2.component />
            </div>
        )
    }

    onClickFoo = () => this.foo2.setState({foo: "foo changed foo2!"})
}

Кажется, для моих целей это работает достаточно хорошо, имейте в виду, что вы не можете изменить состояние свойств, которые вы назначаете для упакованных компонентов на компоненте верхнего уровня, так как каждый упакованный компонент отслеживает свое собственное состояние, но обновляет состояние на верхнем компоненте каждый раз это меняется.


источник
2

На данный момент,

Если следующее состояние зависит от предыдущего состояния, мы рекомендуем использовать вместо него форму функции обновления:

согласно документации https://reactjs.org/docs/react-component.html#setstate , используя:

this.setState((prevState) => {
    return {quantity: prevState.quantity + 1};
});
egidiocs
источник
Спасибо за ссылку / ссылку, которая отсутствовала в других ответах.
Madbreaks
1

Решение

Изменить: Это решение используется для использования синтаксиса распространения . Цель состояла в том, чтобы сделать объект без каких-либо ссылок на него prevState, чтобы prevStateего нельзя было изменить. Но в моем использовании, prevStateказалось, был изменен иногда. Итак, для идеального клонирования без побочных эффектов мы теперь конвертируем prevStateв JSON, а затем снова. (Вдохновение использовать JSON пришло из MDN .)

Помните:

меры

  1. Сделайте копию собственности корневого уровня в stateтом , что вы хотите изменить
  2. Мутировать этот новый объект
  3. Создать объект обновления
  4. Вернуть обновление

Шаги 3 и 4 можно объединить в одну строку.

пример

this.setState(prevState => {
    var newSelected = JSON.parse(JSON.stringify(prevState.selected)) //1
    newSelected.name = 'Barfoo'; //2
    var update = { selected: newSelected }; //3
    return update; //4
});

Упрощенный пример:

this.setState(prevState => {
    var selected = JSON.parse(JSON.stringify(prevState.selected)) //1
    selected.name = 'Barfoo'; //2
    return { selected }; //3, 4
});

Это хорошо следует рекомендациям React. На основании ответа eicksl на аналогичный вопрос.

Тим
источник
1

Решение ES6

Мы устанавливаем государство изначально

this.setState({ selected: { id: 1, name: 'Foobar' } }); 
//this.state: { selected: { id: 1, name: 'Foobar' } }

Мы изменяем свойство на некотором уровне объекта состояния:

const { selected: _selected } = this.state
const  selected = { ..._selected, name: 'Barfoo' }
this.setState({selected})
//this.state: { selected: { id: 1, name: 'Barfoo' } }
Римский
источник
0

React state не выполняет рекурсивное слияние, в setStateто время как ожидает, что в то же время не будет обновлений на месте. Вы должны либо скопировать вложенные объекты / массивы самостоятельно (с помощью array.slice или Object.assign), либо использовать выделенную библиотеку.

Как этот. NestedLink напрямую поддерживает обработку составного состояния React.

this.linkAt( 'selected' ).at( 'name' ).set( 'Barfoo' );

Кроме того, ссылка на selectedили selected.nameможет быть передана повсеместно в виде единого реквизита и изменена там с помощью set.

gaperton
источник
-2

Вы установили начальное состояние?

Я буду использовать мой собственный код, например:

    getInitialState: function () {
        return {
            dragPosition: {
                top  : 0,
                left : 0
            },
            editValue : "",
            dragging  : false,
            editing   : false
        };
    }

В приложении, над которым я работаю, это то, как я устанавливал и использовал состояние. Я верю, что setStateвы можете просто отредактировать все состояния, которые вы хотите индивидуально, как я это называю, так:

    onChange: function (event) {
        event.preventDefault();
        event.stopPropagation();
        this.setState({editValue: event.target.value});
    },

Имейте в виду, что вы должны установить состояние в React.createClassфункции, которую вы вызвалиgetInitialState

asherrard
источник
1
какой миксин вы используете в своем компоненте? Это не работает для меня
Sachin
-3

Я использую переменную tmp для изменения.

changeTheme(v) {
    let tmp = this.state.tableData
    tmp.theme = v
    this.setState({
        tableData : tmp
    })
}
hkongm
источник
2
Здесь tmp.theme = v - состояние мутирования. Так что это не рекомендуемый способ.
almoraleslopez
1
let original = {a:1}; let tmp = original; tmp.a = 5; // original is now === Object {a: 5} Не делайте этого, даже если это еще не доставило вам проблем.
Крис