166

我有以下状态:

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

然后我更新状态:

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

由于setState假设要合并,我希望它是:

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

但相反,它吃掉了 id 并且状态是:

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

这是预期的行为吗?仅更新嵌套状态对象的一个​​属性的解决方案是什么?

4

13 回答 13

147

I think setState() doesn't do recursive merge.

You can use the value of the current state this.state.selected to construct a new state and then call setState() on that:

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

I've used function _.extend() function (from underscore.js library) here to prevent modification to the existing selected part of the state by creating a shallow copy of it.

Another solution would be to write setStateRecursively() which does recursive merge on a new state and then calls replaceState() with it:

setStateRecursively: function(stateUpdate, callback) {
  var newState = mergeStateRecursively(this.state, stateUpdate);
  this.replaceState(newState, callback);
}
于 2013-09-21T15:17:26.650 回答
97

最近向 React.addons 添加了不变性助手,因此,您现在可以执行以下操作:

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

不变性助手文档

于 2014-09-07T18:21:25.950 回答
38

由于许多答案都使用当前状态作为合并新数据的基础,我想指出这可能会中断。状态更改是排队的,不会立即修改组件的状态对象。因此,在处理队列之前引用状态数据会给您提供不反映您在 setState 中所做的未决更改的陈旧数据。从文档:

setState() 不会立即改变 this.state 而是创建一个挂起的状态转换。在调用此方法后访问 this.state 可能会返回现有值。

这意味着在后续调用 setState 时使用“当前”状态作为参考是不可靠的。例如:

  1. 第一次调用 setState,排队改变状态对象
  2. 第二次调用 setState。您的状态使用嵌套对象,因此您想要执行合并。在调用 setState 之前,您会获得当前状态对象。这个对象不反映在第一次调用 setState 时所做的排队更改,因为它仍然是原始状态,现在应该被认为是“陈旧的”。
  3. 执行合并。结果是原始的“陈旧”状态加上您刚刚设置的新数据,不会反映初始 setState 调用的更改。您的 setState 调用将第二次更改排入队列。
  4. React 进程队列。处理第一个 setState 调用,更新状态。处理第二个 setState 调用,更新状态。第二个 setState 的对象现在已经替换了第一个,并且由于您在进行该调用时拥有的数据是陈旧的,因此第二次调用中修改过的陈旧数据破坏了在第一次调用中所做的更改,这些更改丢失了。
  5. 当队列为空时,React 决定是否渲染等。此时您将渲染在第二次 setState 调用中所做的更改,就好像第一次 setState 调用从未发生过一样。

如果您需要使用当前状态(例如,将数据合并到嵌套对象中),setState 可以选择接受函数作为参数而不是对象;该函数在对状态的任何先前更新后被调用,并将状态作为参数传递——因此这可用于保证原子更改尊重先前的更改。

于 2015-12-16T20:14:44.007 回答
11

我不想安装另一个库,所以这里还有另一个解决方案。

代替:

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 上面提到的任何问题。

于 2017-07-21T19:13:28.897 回答
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"}
    }
});
于 2016-08-23T18:12:47.037 回答
4

就目前而言,

如果下一个状态依赖于前一个状态,我们建议使用更新函数形式,而不是:

根据文档https://reactjs.org/docs/react-component.html#setstate,使用:

this.setState((prevState) => {
    return {quantity: prevState.quantity + 1};
});
于 2017-11-26T01:10:50.473 回答
2

我对这种情况的解决方案是使用Immutability helpers,就像另一个答案指出的那样。

由于深入设置状态是一种常见情况,我创建了以下 mixin:

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

这个 mixin 包含在我的大部分组件中,我通常不再setState直接使用。

有了这个mixin,为了达到预期的效果,你需要做的就是setStateinDepth通过以下方式调用函数:

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

了解更多信息:

  • 关于 mixins 在 React 中是如何工作的,请参阅官方文档
  • 关于传递参数的语法,setStateinDepth请参阅 Immutability Helpers文档
于 2015-10-30T20:38:37.260 回答
2

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' } }
于 2018-12-19T23:22:58.853 回答
2

解决方案

编辑:这个解决方案曾经使用传播语法。目标是创建一个没有任何引用的对象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对类似问题的回答。

于 2018-07-30T18:36:08.267 回答
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!"})
}

似乎对我的目的来说工作得很好,请记住,尽管您不能更改分配给顶级组件的包装组件的属性的状态,因为每个包装组件都在跟踪自己的状态,但会更新顶级组件的状态每次它改变。

于 2016-05-08T02:37:11.003 回答
0

React 状态不执行递归合并,setState同时期望不会有就地状态成员更新。您要么必须自己复制封闭的对象/数组(使用 array.slice 或 Object.assign),要么使用专用库。

像这个。NestedLink直接支持复合 React 状态的处理。

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

此外,到selectedor的链接selected.name可以作为单个道具传递到任何地方,并在那里用set.

于 2017-04-19T19:58:06.893 回答
-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

于 2013-11-22T22:29:32.880 回答
-3

我使用 tmp var 进行更改。

changeTheme(v) {
    let tmp = this.state.tableData
    tmp.theme = v
    this.setState({
        tableData : tmp
    })
}
于 2015-07-06T09:34:57.847 回答