This. setState 并没有像我期望的那样合并州

我有以下状态:

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

然后我更新状态:

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

因为 setState应该合并,所以我希望它是:

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

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

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

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

75427 次浏览

我认为 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);
}

设置初始状态了吗?

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

    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函数中设置状态

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

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

不可变性帮助文档

我用 tmp 变量来改变。

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

对于这种情况,我的解决方案是使用,就像另一个答案指出的那样,永恒的帮手

因为深入设置状态是一种常见的情况,所以我创建了以下混合:

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

这个 mix in 包含在我的大多数组件中,我通常不再直接使用 setState

使用这个混合函数,为了达到预期的效果,您所需要做的就是以下面的方式调用函数 setStateinDepth:

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

了解更多信息:

  • 有关在 React 中混合器的工作方式,请参见 官方文件
  • 关于传递给 setStateinDepth的参数的语法,请参见 Immutability Helpers 文件

由于许多答案使用当前状态作为合并新数据的基础,我想指出的是,这可能会中断。状态更改将排队,并且不会立即修改组件的状态对象。因此,在处理队列之前引用状态数据将使您获得不能反映您在 setState 中所做的挂起的更改的过时数据。来自文件:

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

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

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

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

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

然后,对于 top 状态上的每个复杂属性,创建一个 StateWrated 类。你可以在这里的构造函数中设置默认的道具,当类初始化的时候它们就会被设置,你可以参考本地状态来设置值,参考本地函数,然后把它传递给链:

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

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

基于@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 Immutability 示例:

import update from "react-addons-update";


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

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

NestedLink直接支持处理复合反应状态。

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

此外,到 selectedselected.name的链接可以作为一个单独的支柱传递到任何地方,并在那里与 set一起修改。

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

而不是:

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

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

从现在开始,

如果下一个状态取决于前一个状态,我们建议使用 更新程序函数表单,而不是:

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

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

解决方案

编辑: 此解决方案过去使用 传播语法。目标是创建一个没有任何对 prevState引用的对象,这样就不会修改 prevState。但在我的使用中,prevState似乎有时会被修改。因此,为了实现无副作用的完美克隆,我们现在将 prevState转换为 JSON,然后再转换回来。(使用 JSON 的灵感来自 MDN。)

记住:

  • 状态更新是浅层的 : < a href = “ https://reactjs.org/docs/response-Component ent.html # setstate”rel = “ nofollow noReferrer”> 它们只深入一层
  • State 不应该直接突变为 ,而应该使用 < a href = “ https://reactjs.org/docs/response-Component ent.html # setstate”rel = “ nofollow noReferrer”> setState(prevState => stateChange)

步骤

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

这很好地遵循了反应准则。基于 Eicksl 的对类似问题的回答。

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