简体   繁体   English

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

[英]this.setState isn't merging states as I would expect

I have the following state:我有以下状态:

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

Then I update the state:然后我更新状态:

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

Since setState is suppose to merge I would expect it to be:由于setState应该合并,我希望它是:

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

But instead it eats the id and the state is:但相反,它吃掉了 id,状态是:

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

Is this expected behavior and what's the solution to update only one property of a nested state object?这是预期的行为吗?仅更新嵌套状态对象的一个​​属性的解决方案是什么?

I think setState() doesn't do recursive merge.我认为setState()不会进行递归合并。

You can use the value of the current state this.state.selected to construct a new state and then call setState() on that:您可以使用当前状态this.state.selected的值来构造一个新状态,然后在其上调用setState()

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

Another solution would be to write setStateRecursively() which does recursive merge on a new state and then calls replaceState() with it:另一种解决方案是编写setStateRecursively() ,它对新状态进行递归合并,然后用它调用replaceState()

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

Immutability helpers were recently added to React.addons, so with that, you can now do something like:不变性助手最近添加到 React.addons 中,因此,您现在可以执行以下操作:

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

Immutability helpers documentation .不变性助手文档

Since many of the answers use the current state as a basis for merging in new data, I wanted to point out that this can break.由于许多答案使用当前状态作为合并新数据的基础,我想指出这可能会中断。 State changes are queued, and do not immediately modify a component's state object.状态更改排队,并且不会立即修改组件的状态对象。 Referencing state data before the queue has been processed will therefore give you stale data that does not reflect the pending changes you made in setState.因此,在处理队列之前引用状态数据将为您提供不反映您在 setState 中所做的未决更改的陈旧数据。 From the docs:从文档:

setState() does not immediately mutate this.state but creates a pending state transition. setState() 不会立即改变 this.state 而是创建一个挂起的状态转换。 Accessing this.state after calling this method can potentially return the existing value.调用此方法后访问 this.state 可能会返回现有值。

This means using "current" state as a reference in subsequent calls to setState is unreliable.这意味着在对 setState 的后续调用中使用“当前”状态作为参考是不可靠的。 For example:例如:

  1. First call to setState, queuing a change to state object第一次调用 setState,排队改变状态对象
  2. Second call to setState.第二次调用 setState。 Your state uses nested objects, so you want to perform a merge.您的状态使用嵌套对象,因此您要执行合并。 Before calling setState, you get current state object.在调用 setState 之前,您会获得当前状态对象。 This object does not reflect queued changes made in first call to setState, above, because it's still the original state, which should now be considered "stale".这个对象不反映在第一次调用 setState 时所做的排队更改,因为它仍然是原始状态,现在应该被视为“过时”。
  3. Perform merge.执行合并。 Result is original "stale" state plus new data you just set, changes from initial setState call are not reflected.结果是原始的“陈旧”状态加上您刚刚设置的新数据,不会反映初始 setState 调用的更改。 Your setState call queues this second change.您的 setState 调用将第二次更改排队。
  4. React processes queue.反应进程队列。 First setState call is processed, updating state.处理第一个 setState 调用,更新状态。 Second setState call is processed, updating state.处理第二个 setState 调用,更新状态。 The second setState's object has now replaced the first, and since the data you had when making that call was stale, the modified stale data from this second call has clobbered the changes made in the first call, which are lost.第二个 setState 的对象现在已经取代了第一个,并且由于您在进行该调用时拥有的数据已经过时,因此第二次调用中修改后的过时数据已经破坏了在第一次调用中所做的更改,这些更改已丢失。
  5. When queue is empty, React determines whether to render etc. At this point you will render the changes made in the second setState call, and it will be as though the first setState call never happened.当队列为空时,React 决定是否渲染等。此时您将渲染在第二个 setState 调用中所做的更改,就好像第一个 setState 调用从未发生过一样。

If you need to use the current state (eg to merge data into a nested object), setState alternatively accepts a function as an argument instead of an object;如果您需要使用当前状态(例如将数据合并到嵌套对象中),setState 或者接受一个函数作为参数而不是一个对象; the function is called after any previous updates to state, and passes the state as an argument -- so this can be used to make atomic changes guaranteed to respect previous changes.该函数在任何先前的状态更新后被调用,并将状态作为参数传递——因此这可用于保证原子更改尊重先前的更改。

I didn't want to install another library so here's yet another solution.我不想安装另一个库,所以这是另一个解决方案。

Instead of:代替:

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

Do this instead:改为这样做:

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

Or, thanks to @icc97 in the comments, even more succinctly but arguably less readable:或者,感谢评论中的@icc97,更简洁但可以说可读性更差:

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

Also, to be clear, this answer doesn't violate any of the concerns that @bgannonpl mentioned above.另外,要明确的是,这个答案没有违反@bgannonpl 上面提到的任何问题。

Preserving the previous state based on @bgannonpl answer:保留基于@bgannonpl 答案的先前状态:

Lodash example:洛达什示例:

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

To check that it's worked properly, you can use the second parameter function callback:要检查它是否正常工作,您可以使用第二个参数函数回调:

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

I used merge because extend discards the other properties otherwise.我使用了merge因为否则extend会丢弃其他属性。

React Immutability example:反应不变性示例:

import update from "react-addons-update";

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

As of right now,截至目前,

If the next state depends on the previous state, we recommend using the updater function form, instead:如果下一个状态依赖于前一个状态,我们建议使用 updater 函数形式,而不是:

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

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

My solution for this kind of situation is to use, like another answer pointed out, the Immutability helpers .我对这种情况的解决方案是使用Immutability helpers ,就像另一个答案指出的那样。

Since setting the state in depth is a common situation, I've created the folowing mixin:由于深度设置状态是一种常见情况,因此我创建了以下 mixin:

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

This mixin is included in most of my components and I generally do not use setState directly anymore.这个 mixin 包含在我的大多数组件中,我通常不再直接使用setState

With this mixin, all you need to do in order to achieve the desired effect is to call the function setStateinDepth in the following way:有了这个mixin,你需要做的就是以如下方式调用函数setStateinDepth来达到预期的效果:

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

For more information:了解更多信息:

  • On how mixins work in React, see the official documentation关于 mixin 在 React 中的工作原理,请参阅官方文档
  • On the syntax of the parameter passed to setStateinDepth see the Immutability Helpers documentation .有关传递给setStateinDepth的参数的语法,请参阅 Immutability Helpers文档

I am using es6 classes, and I ended up with several complex objects on my top state and was trying to make my main component more modular, so i created a simple class wrapper to keep the state on the top component but allow for more local logic.我正在使用 es6 类,最终在我的顶层状态中有几个复杂的对象,并试图使我的主要组件更加模块化,所以我创建了一个简单的类包装器来保持顶层组件上的状态,但允许更多的本地逻辑.

The wrapper class takes a function as its constructor that sets a property on the main component state.包装器类将一个函数作为其构造函数,该函数在主组件状态上设置一个属性。

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

Then for each complex property on the top state, i create one StateWrapped class.然后对于顶部状态的每个复杂属性,我创建一个 StateWrapped 类。 You can set the default props in the constructor here and they will be set when the class is initialised, you can refer to the local state for values and set the local state, refer to local functions, and have it passed up the chain:您可以在此处的构造函数中设置默认道具,它们将在类初始化时设置,您可以引用本地状态的值并设置本地状态,引用本地函数,并将其传递到链中:

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


}

So then my top level component just needs the constructor to set each class to it's top level state property, a simple render, and any functions that communicate cross-component.那么我的顶级组件只需要构造函数来将每个类设置为它的顶级状态属性、一个简单的渲染以及任何与跨组件通信的函数。

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

Seems to work quite well for my purposes, bear in mind though you can't change the state of the properties you assign to wrapped components at the top level component as each wrapped component is tracking its own state but updating the state on the top component each time it changes.似乎对我的目的来说效果很好,但请记住,尽管您无法更改分配给顶级组件的包装组件的属性状态,因为每个包装组件都在跟踪自己的状态,但会更新顶级组件的状态每次都变了。

Solution解决方案

Edit: This solution used to use spread syntax .编辑:此解决方案用于使用传播语法 The goal was make an object without any references to prevState , so that prevState wouldn't be modified.目标是创建一个不引用prevState的对象,这样就不会修改prevState But in my usage, prevState appeared to be modified sometimes.但是在我的使用中, prevState似乎有时会被修改。 So, for perfect cloning without side effects, we now convert prevState to JSON, and then back again.因此,为了完美克隆而没有副作用,我们现在将prevState转换为 JSON,然后再返回。 (Inspiration to use JSON came from MDN .) (使用 JSON 的灵感来自MDN 。)

Remember:记住:

Steps脚步

  1. Make a copy of the root-level property of state that you want to change复制要更改的state根级属性
  2. Mutate this new object改变这个新对象
  3. Create an update object创建更新对象
  4. Return the update返回更新

Steps 3 and 4 can be combined on one line.步骤 3 和 4 可以合并为一行。

Example例子

this.setState(prevState => {
    var newSelected = JSON.parse(JSON.stringify(prevState.selected)) //1
    newSelected.name = 'Barfoo'; //2
    var update = { selected: newSelected }; //3
    return update; //4
});

Simplified example:简化示例:

this.setState(prevState => {
    var selected = JSON.parse(JSON.stringify(prevState.selected)) //1
    selected.name = 'Barfoo'; //2
    return { selected }; //3, 4
});

This follows the React guidelines nicely.这很好地遵循了 React 指南。 Based on eicksl's answer to a similar question.基于eicksl对类似问题回答。

ES6 solution ES6解决方案

We set the state initially我们最初设置状态

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

We are changeing a property on some level of the state object:我们正在更改状态对象某个级别的属性:

const { selected: _selected } = this.state
const  selected = { ..._selected, name: 'Barfoo' }
this.setState({selected})
//this.state: { selected: { id: 1, name: 'Barfoo' } }

React state doesn't perform the recursive merge in setState while expects that there won't be in-place state member updates at the same time. React 状态不会在setState执行递归合并,同时预计不会有就地状态成员更新。 You either have to copy enclosed objects/arrays yourself (with array.slice or Object.assign) or use the dedicated library.您必须自己复制封闭的对象/数组(使用 array.slice 或 Object.assign)或使用专用库。

Like this one.像这个。 NestedLink directly supports handling of the compound React state. NestedLink直接支持处理复合 React 状态。

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

Also, the link to the selected or selected.name can be passed everywhere as a single prop and modified there with set .此外,指向selectedselected.name的链接可以作为单个 prop 传递到任何地方,并在那里使用set修改。

have you set the initial state?你设置初始状态了吗?

I'll use some of my own code for example:我将使用一些我自己的代码,例如:

    getInitialState: function () {
        return {
            dragPosition: {
                top  : 0,
                left : 0
            },
            editValue : "",
            dragging  : false,
            editing   : false
        };
    }

In an app I'm working on, this is how I've been setting and using state.在我正在开发的应用程序中,这就是我设置和使用状态的方式。 I believe on setState you can then just edit whatever states you want individually I've been calling it like so:我相信在setState您可以单独编辑您想要的任何状态,我一直这样称呼它:

    onChange: function (event) {
        event.preventDefault();
        event.stopPropagation();
        this.setState({editValue: event.target.value});
    },

Keep in mind you have to set the state within the React.createClass function that you called getInitialState请记住,您必须在您调用getInitialStateReact.createClass函数中设置状态

I use the tmp var to change.我使用 tmp var 进行更改。

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM