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


159

我有以下状态:

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

然后我更新状态:

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

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

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

但是它吃了id,状态为:

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

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

Answers:


143

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

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

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

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

另一个解决方案是编写setStateRecursively()对新状态进行递归合并的新状态,然后对其进行调用replaceState()

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

7
如果您多次执行此操作,是否可靠工作?或者有机会做出反应,将replaceState调用排入队列,最后一个将获胜吗?
edoloughlin

111
对于喜欢使用扩展并且也使用babel的人们,您可以将this.setState({ selected: { ...this.state.selected, name: 'barfoo' } })其翻译为this.setState({ selected: _extends({}, this.state.selected, { name: 'barfoo' }) });
Pickels,2015年

8
@Pickels这很棒,对React很惯用,不需要underscore/ lodash。请把它分解成自己的答案。
ericsoco

14
this.state 不一定是最新的。使用extend方法可能会得到意外的结果。将更新功能传递给setState是正确的解决方案。
斯特罗姆

1
@ EdwardD'Souza这是lodash库。通常将其作为下划线调用,与将jQuery通常作为Dollarsign调用的方式相同。(因此,是_.extend()。)
mjk

96

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

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

不变性助手文档



3
@thedanotto似乎React.addons.update()已弃用该方法,但替换库github.com/kolodny/immutability-helper包含等效update()功能。
Bungle

37

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

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

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

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

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


5
是否有一个示例或更多文档说明了如何执行此操作?afaik,没有人考虑到这一点。
Josef.B

@Dennis在下面尝试我的答案。
马丁·道森

我看不出问题出在哪里。仅当您在调用setState之后尝试访问“新”状态时,才会出现所描述的内容。那是一个完全不同的问题,与OP问题无关。在调用setState之前访问旧状态,以便您可以构造一个新的状态对象永远不会成为问题,而实际上正是React所期望的。
nextgentech

@nextgentech “在调用setState之前访问旧状态,这样您就可以构造新的状态对象永远不会成为问题” –您弄错了。我们谈论的是覆盖基于状态this.state,这可能是过时的(或者更确切地说,等待排队更新),当你访问它。
Madbreaks '18

1
@Madbreaks好吧,是的,在重新阅读官方文档后,我发现指定您必须使用setState函数形式来可靠地更新基于先前状态的状态。就是说,到目前为止,如果不尝试在同一函数中多次调用setState,我从来没有遇到过问题。感谢您的回复,让我重新阅读了规格。
nextgentech '18年

10

我不想安装另一个库,因此这里是另一个解决方案。

代替:

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


投票,检查。只是想知道为什么不这样做呢?this.setState({ selected: Object.assign(this.state.selected, { name: "Barfoo" }));
Justus Romijn

1
@JustusRomijn不幸的是,那么您将直接修改当前状态。 Object.assign(a, b)从中获取属性b,插入/覆盖属性,a然后返回a。如果您直接修改状态,React的人就会感到很沮丧。
Ryan Shillington

究竟。请注意,这仅适用于浅拷贝/分配。
Madbreaks '18年

1
@JustusRomijn 如果将第一个参数添加为,则可以按照一行中的MDN分配执行此操作。这不会改变原始状态。{}this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });
icc97 '18

@ icc97不错!我将其添加到答案中。
瑞安·希灵顿

8

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

2

我针对这种情况的解决方案是使用,如另一个答案指出的那样,不变性助手

由于深度设置状态是一种常见情况,因此我创建了以下混合:

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

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

使用此mixin,要获得所需的效果,您所需要做的就是以setStateinDepth以下方式调用该函数:

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

想要查询更多的信息:

  • 有关mixin在React中的工作方式,请参阅官方文档
  • 关于传递的参数的语法,setStateinDepth请参见Immutability Helpers 文档

抱歉,您的答复很晚,但是您的Mixin示例似乎很有趣。但是,如果我有2个嵌套状态,例如this.state.test.one和this.state.test.two。仅更新其中之一而删除另一个是正确的吗?当我更新第一个时,如果第二个i处于状态,则将其删除...
mamruoc 2015年

hy @mamruoc,this.state.test.two在您this.state.test.one使用进行更新后仍然会存在setStateInDepth({test: {one: {$set: 'new-value'}}})。我在所有组件中都使用了此代码,以避开React的有限setState功能。
多里安

1
我找到了解决方案。我开始时this.state.test是一个数组。更改为对象,解决了它。
mamruoc 2015年

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

似乎可以很好地实现我的目的,请记住,尽管您不能更改分配给顶级组件上的包装组件的属性的状态,因为每个包装组件都在跟踪自己的状态,但会更新顶级组件上的状态每次改变。



1

编辑:此解决方案用于使用传播语法。目标是使对象没有任何对的引用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解决方案

我们最初设置状态

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

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

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

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

另外,指向selected或的链接selected.name可以作为单个prop传递到任何地方,并使用进行修改set


-2

您设置了初始状态吗?

我将使用一些自己的代码作为示例:

    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
您在组件中使用了什么mixin?这对我不起作用
Sachin

-3

我使用tmp var进行更改。

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

2
这里tmp.theme = v是变异状态。因此,这不是推荐的方法。
almoraleslopez 2015年

1
let original = {a:1}; let tmp = original; tmp.a = 5; // original is now === Object {a: 5} 即使尚未给您问题,也不要这样做。
克里斯(Chris
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.