【问题标题】:this.setState isn't merging states as I would expectthis.setState 没有像我期望的那样合并状态
【发布时间】:2013-09-26 20:35:31
【问题描述】:

我有以下状态:

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

然后我更新状态:

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

由于setState 应该合并,我希望它是:

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

但是它却吃掉了 id 并且状态是:

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

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

【问题讨论】:

    标签: javascript reactjs


    【解决方案1】:

    我认为setState() 不会进行递归合并。

    您可以使用当前状态this.state.selected 的值来构造一个新状态,然后在其上调用setState()

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

    我在这里使用了函数 _.extend() 函数(来自 underscore.js 库),通过创建浅表副本来防止修改现有的 selected 状态部分。

    另一种解决方案是编写setStateRecursively(),它在新状态上进行递归合并,然后用它调用replaceState()

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

    【讨论】:

    • 如果您多次执行此操作是否可靠,或者是否有可能 react 会将 replaceState 调用排队并且最后一个将获胜?
    • 对于喜欢使用 extend 并且也在使用 babel 的人,您可以使用 this.setState({ selected: { ...this.state.selected, name: 'barfoo' } }) 将其转换为 this.setState({ selected: _extends({}, this.state.selected, { name: 'barfoo' }) });
    • @Pickels 这很棒,非常适合 React,并且不需要 underscore/lodash。请把它变成自己的答案。
    • this.stateisn't necessarily up-to-date。使用 extend 方法可以获得意想不到的结果。将更新函数传递给setState 是正确的解决方案。
    • @EdwardD'Souza 这是 lodash 库。它通常作为下划线调用,就像 jQuery 通常作为美元符号调用一样。 (所以,它是 _.extend()。)
    【解决方案2】:

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

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

    Immutability helpers documentation.

    【讨论】:

    【解决方案3】:

    由于许多答案都使用当前状态作为合并新数据的基础,我想指出这可能会中断。状态更改是排队的,不会立即修改组件的状态对象。因此,在处理队列之前引用状态数据会给您提供不反映您在 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 可以选择接受函数作为参数而不是对象;该函数在之前的任何状态更新后被调用,并将状态作为参数传递——因此这可用于保证原子更改尊重之前的更改。

    【讨论】:

    • 是否有关于如何执行此操作的示例或更多文档? afaik,没有人考虑到这一点。
    • @Dennis 在下面试试我的答案。
    • 我看不出问题出在哪里。只有在调用 setState 后尝试访问“新”状态时,才会发生您所描述的情况。这是一个完全不同的问题,与 OP 问题没有任何关系。在调用 setState 之前访问旧状态以便您可以构造一个新的状态对象永远不会成为问题,实际上这正是 React 所期望的。
    • @nextgentech “在调用 setState 之前访问旧状态,这样你就可以构造一个新的状态对象永远不会成为问题” - 你错了。我们正在讨论基于this.state覆盖 状态,当您访问它时,它可能是陈旧的(或者更确切地说,等待队列更新)。
    • @Madbreaks 好的,是的,在重新阅读官方文档后,我看到它指定您必须使用 setState 的函数形式来可靠地根据以前的状态更新状态。也就是说,如果不尝试在同一个函数中多次调用 setState,我迄今为止从未遇到过问题。感谢您的回复让我重新阅读规范。
    【解决方案4】:

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

    代替:

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

    改为这样做:

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

    或者,感谢 cmets 中的@icc97,更简洁但可以说可读性较差:

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

    另外,需要明确的是,这个答案没有违反@bgannonpl 上面提到的任何问题。

    【讨论】:

    • 支持,检查。只是想知道为什么不这样做呢? this.setState({ selected: Object.assign(this.state.selected, { name: "Barfoo" }));
    • @JustusRomijn 不幸的是,您将直接修改当前状态。 Object.assign(a, b)b 获取属性,在a 中插入/覆盖它们,然后返回a。如果你直接修改状态,React 的人会很自大。
    • 没错。请注意,这仅适用于浅拷贝/分配。
    • @JustusRomijn 如果您将{} 添加为第一个参数:this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });,您可以在一行中按照MDN assign 执行此操作。这不会改变原始状态。
    • @icc97 不错!我将其添加到我的答案中。
    【解决方案5】:

    根据@bgannonpl 回答保留之前的状态:

    Lodash 示例:

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

    要检查它是否正常工作,可以使用第二个参数函数回调:

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

    我使用了merge,因为extend 会丢弃其他属性。

    React 不变性示例:

    import update from "react-addons-update";
    
    this.setState((previousState) => update(previousState, {
        selected:
        { 
            name: {$set: "Barfood"}
        }
    });
    

    【讨论】:

      【解决方案6】:

      目前,

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

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

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

      【讨论】:

      • 感谢其他答案中缺少的引用/链接。
      【解决方案7】:

      我对这种情况的解决方案是使用,就像另一个答案指出的那样,the 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' }}})
      

      更多信息:

      【讨论】:

      • 抱歉回复晚了,但您的 Mixin 示例似乎很有趣。但是,如果我有 2 个嵌套状态,例如this.state.test.one 和 this.state.test.two。只更新其中一个而删除另一个是否正确?当我更新第一个时,如果第二个处于状态,它将被删除...
      • hy @mamruoc, this.state.test.two 在您使用 setStateInDepth({test: {one: {$set: 'new-value'}}}) 更新 this.state.test.one 后仍然存在。我在我的所有组件中都使用了这段代码,以绕过 React 有限的 setState 函数。
      • 我找到了解决方案。我将this.state.test作为一个数组发起。改成Objects,解决了。
      【解决方案8】:

      我正在使用 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!"})
      }
      

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

      【讨论】:

        【解决方案9】:

        解决方案

        编辑:这个解决方案曾经使用spread syntax。目标是创建一个不引用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's 对类似问题的回答。

        【讨论】:

          【解决方案10】:

          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' } }
          

          【讨论】:

            【解决方案11】:

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

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

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

            此外,selectedselected.name 的链接可以作为单个 prop 传递到任何地方,并在那里使用 set 进行修改。

            【讨论】:

              【解决方案12】:

              您是否设置了初始状态?

              我将使用一些我自己的代码,例如:

                  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});
                  },
              

              请记住,您必须在调用 getInitialStateReact.createClass 函数中设置状态

              【讨论】:

              • 您在组件中使用了什么 mixin?这对我不起作用
              【解决方案13】:

              我使用 tmp var 进行更改。

              changeTheme(v) {
                  let tmp = this.state.tableData
                  tmp.theme = v
                  this.setState({
                      tableData : tmp
                  })
              }
              

              【讨论】:

              • 这里 tmp.theme = v 是变异状态。所以这不是推荐的方式。
              • let original = {a:1}; let tmp = original; tmp.a = 5; // original is now === Object {a: 5} 不要这样做,即使它还没有给你带来问题。
              猜你喜欢
              • 1970-01-01
              • 2016-12-03
              • 1970-01-01
              • 2019-05-08
              • 1970-01-01
              • 2019-12-04
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多