this.setState 并没有像我期望的那样合并状态

IT技术 javascript reactjs
2021-01-25 20:44:09

我有以下状态:

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

然后我更新状态:

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

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

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

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

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

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

6个回答

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

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

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

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

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

setStateRecursively: function(stateUpdate, callback) {
  var newState = mergeStateRecursively(this.state, stateUpdate);
  this.replaceState(newState, callback);
}
@EdwardD'Souza 这是 lodash 库。它通常作为下划线调用,与 jQuery 通常作为美元符号调用的方式相同。(所以,它是 _.extend()。)
2021-03-20 20:44:09
如果您不止一次这样做,这是否可靠地工作,或者是否有机会 react 将 replaceState 调用排队,最后一个会获胜?
2021-03-30 20:44:09
对于喜欢使用扩展并且也使用 babel 的人,您可以将this.setState({ selected: { ...this.state.selected, name: 'barfoo' } })其转换为this.setState({ selected: _extends({}, this.state.selected, { name: 'barfoo' }) });
2021-04-02 20:44:09
this.state 不一定是最新的使用扩展方法可以获得意想不到的结果。将更新函数传递给setState是正确的解决方案。
2021-04-10 20:44:09
@Pickels 这很棒,非常适合 React 并且不需要underscore/ lodash请把它分解成它自己的答案。
2021-04-11 20:44:09

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

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

不变性助手文档

@thedanotto 看起来该React.addons.update()方法已被弃用,但替换库github.com/kolodny/immutability-helper包含等效的update()功能。
2021-03-20 20:44:09
2021-04-05 20:44:09

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

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

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

代替:

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

@JustusRomijn为此,您可以按MDN分配在一个行,如果你添加{}的第一个参数:this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });这不会改变原始状态。
2021-03-15 20:44:09
@JustusRomijn 不幸的是,您将直接修改当前状态。 Object.assign(a, b)从 中获取属性b,在其中插入/覆盖它们a,然后返回a如果你直接修改状态,React 会让人感到骄傲。
2021-03-23 20:44:09
点赞,检查。只是想知道为什么不这样做?this.setState({ selected: Object.assign(this.state.selected, { name: "Barfoo" }));
2021-04-03 20:44:09
@icc97 不错!我将其添加到我的答案中。
2021-04-06 20:44:09
确切地。请注意,这仅适用于浅复制/分配。
2021-04-13 20:44:09

保留基于@bgannonpl 答案的先前状态:

洛达什示例:

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