it-swarm.com.ru

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

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

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

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

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

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

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

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

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

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

144
Pickels

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

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

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

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

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

setStateRecursively: function(stateUpdate, callback) {
  var newState = mergeStateRecursively(this.state, stateUpdate);
  this.replaceState(newState, callback);
}
133
andreypopp

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

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

Документация помощников по неизменяемости: http://facebook.github.io/react/docs/update.html

94
user3874611

Поскольку многие ответы используют текущее состояние в качестве основы для объединения новых данных, я хотел бы отметить, что это может сломаться. Изменения состояния ставятся в очередь и не сразу изменяют объект состояния компонента. Поэтому ссылка на данные состояния до обработки очереди даст вам устаревшие данные, которые не отражают ожидающие изменения, сделанные вами в 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 альтернативно принимает функцию в качестве аргумента вместо объекта; функция вызывается после любых предыдущих обновлений состояния и передает состояние в качестве аргумента - так что это может быть использовано для внесения атомарных изменений, гарантирующих уважение предыдущих изменений.

33
bgannonpl

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

Вместо:

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

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

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

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

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

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

9
Ryan Shillington

Сохранение предыдущего состояния на основе ответа @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"}
    }
});
7
Martin Dawson

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

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

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

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

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

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

Для дополнительной информации:

2
Dorian

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

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

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

this.setState((prevState) => {
    return {quantity: prevState.quantity + 1};
});
2
egidiocs

Решение

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

Помните:

Меры

  1. Сделайте копию свойства root-level для 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's ответ на аналогичный вопрос.

1
Tim

Я использую классы 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!"})
}

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

1
user1641172

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

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

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

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

0
gaperton

Решение 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
Roman