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

142

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

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

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

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

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

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

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

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

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

Теги:

13 ответов

132
Лучший ответ

Я думаю, что 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);
}
  • 7
    Надежно ли это работает, если вы делаете это более одного раза или есть вероятность того, что реакция отразится в очереди на вызовы replaceState, а последний выиграет?
  • 108
    Для людей, которые предпочитают использовать extend и которые также используют babel, вы можете сделать this.setState({ selected: { ...this.state.selected, name: 'barfoo' } }) который переводится в this.setState({ selected: _extends({}, this.state.selected, { name: 'barfoo' }) });
Показать ещё 6 комментариев
90

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

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

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

33

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

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

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

  • Первый вызов setState, очередь на изменение объекта состояния
  • Второй вызов setState. Ваше государство использует вложенные объекты, поэтому вы хотите выполнить слияние. Перед вызовом setState вы получаете текущий объект состояния. Этот объект не отражает изменения в очереди, сделанные при первом вызове setState, выше, поскольку он все еще является исходным состоянием, которое теперь должно считаться "устаревшим".
  • Выполнить слияние. Результат - это оригинальное состояние "старое" плюс новые данные, которые вы только что установили, изменения от начального вызова SetState не отражаются. Ваш вызов setState приостанавливает это второе изменение.
  • Реагирует очередь процессов. Первый вызов setState обрабатывается, обновляя состояние. Второй вызов setState обрабатывается, обновляя состояние. Второй объект setState теперь заменил первый, и поскольку данные, которые были у вас при выполнении этого вызова, были устаревшими, измененные устаревшие данные этого второго вызова сбивали изменения, сделанные в первом вызове, которые теряются.
  • Когда очередь пуста, React определяет, будет ли отображаться и т.д. На этом этапе вы будете отображать изменения, сделанные во втором вызове setState, и это будет так, как будто первый вызов setState никогда не происходил.

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

  • 4
    Есть ли пример или несколько документов о том, как это сделать? afaik, никто не принимает это во внимание.
  • 0
    Действительно, больше информации приветствуется.
Показать ещё 6 комментариев
8

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

Вместо:

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

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

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

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

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

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

  • 0
    Upvote, проверьте. Просто интересно, почему бы не сделать это так? this.setState({ selected: Object.assign(this.state.selected, { name: "Barfoo" }));
  • 1
    @JustusRomijn К сожалению, тогда вы будете изменять текущее состояние напрямую. Object.assign(a, b) берет атрибуты из b , вставляет / перезаписывает их в a и затем возвращает a . Реагировать на людей можно, если вы измените состояние напрямую.
Показать ещё 3 комментария
7

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

Как сейчас,

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

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

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

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

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

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

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

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

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

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

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

Решение

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

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

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!"})
}

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

0

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

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

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

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

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

-3

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

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

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

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

    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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню