React.js: setState覆盖而非合并

70

我对React.JS还比较新,目前正在通过构建类似砌体样式的布局进行尝试。

我将每个元素渲染到DOM中,然后需要循环遍历每个项并根据前面的元素应用x和y位置。

初始模型如下:

[
  {
    "title": "The Forrest",
    "description": "some cool text",
    "imgSmallSrc": "/img/img4-small.jpg",
    "imgAlt": "Placeholder image",
    "tags": [
        "Design",
        "Mobile",
        "Responsive"
    ],
    "date": 1367154709885,
    "podStyle": {
      "width": 253
    }
  }
]

(为了简洁起见,我只展示了一个条目。)

循环结束并获得我的x和y数据后,我想将其应用于podStyle对象。我使用以下数据调用setState()

[
  {
    "podStyle": {
      "x": 0,
      "y": 0,
      "height": 146,
      "width": 253
    }
  }
]

这似乎会从模型中删除所有当前数据,并只保留podStyle数据。我是否误解了此合并的工作方式?


3
在React的插件中尝试使用"immutability helpers",以声明方式修改复杂状态对象,这将使内容更易于理解。请参阅链接:https://facebook.github.io/react/docs/update.html - Ross Allen
3个回答

84
如果您的状态是一个对象:
getInitialState: function() {
  return { x: 0, y: 0 };
}

你可以使用setState在该对象上设置单个键:

this.setState({ x: 1 }); // y still == 0

React不会对你的状态进行智能合并;例如,以下操作是无效的:

React不会智能地将您的状态进行合并,例如,这样做是行不通的:

getInitialState: function() {
  return {
    point: { x: 0, y: 0 },
    radius: 10
  };
}

this.setState({point: {x: 1}});
// state is now == {point: {x: 1}, radius: 10} (point.y is gone)

[编辑]

如 @ssorallen 所提到的,您可以使用不可变性助手来获得您想要的效果:

var newState = React.addons.update(this.state, {
  point: { x: {$set: 10} }
});
this.setState(newState);

请参考这个JSFiddle的示例:http://jsfiddle.net/BinaryMuse/HW6w5/


所以我相当确定我正在按照你的第二个示例设置状态,但是在这种情况下,“半径”被删除了。今晚回家后,我会试着玩一下,并确保我正确地设置了。感谢你的帮助Brandon! - DanV
1
@DanV 这里有一个 JSFiddle,用于演示我回答中的代码。http://jsfiddle.net/BinaryMuse/HW6w5/ 我还添加了一个按钮来使用 ssorallen 提到的不可变性帮助程序,它可以实现你想要做的事情。 - Michelle Tilley
非常感谢你的帮助,Brandon。这确实让我朝着正确的方向前进了。最终,我使用了$merge助手作为我的模型略有不同,并且我还将更新应用于多个子项。虽然很好用。 - DanV

23
合并是浅层的,所以 this.setState({point}) 保留了(ed:this.state.radius),但完全替换了(ed:this.state.point)。

https://facebook.github.io/react/docs/state-and-lifecycle.html#state-updates-are-merged

为了提供ES7 +视角的答案,使用 transform-object-rest-spread 替代 Object.assign()
class MyComponent extends React.Component {
    state = {
        point: { 
            x: 0, 
            y: 0,
        },
        radius: 10,
    }

    handleChange = () => {
        this.setState((prevState, props) => ({
            point: {
                // rest operator (...) expands out to:
                ...prevState.point, // x:0, y:0,
                y: 1, // overwrites old y
            },
            // radius is not overwritten by setState
        }));
    }

    render() {
        // omitted
    }
}

.babelrc(还需要来自Babel预设阶段2的transform-class-properties

{
    "presets": ["es2015", "stage-2", "react"],
    "plugins": ["transform-object-rest-spread"],
}

更新于2018年4月22日

@sheljohn(感谢!)指出,在setState内部引用this.state是不可靠的:

因为this.propsthis.state可能会异步更新,所以您不应该依赖它们的值来计算下一个状态。

...

为了解决这个问题,使用setState()的第二种形式,它接受一个函数而不是一个对象。该函数将接收前一个状态作为第一个参数,并在更新应用时传递的属性作为第二个参数。

https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous


谢谢!我认为这是一个惊人的解决方案。 - Balázs Orbán
2
我对React还很陌生,但是这个似乎不对;handleChange应该调用this.setState((prevState, props) => ({ ... // use prevState.point })) - Jonathan H
哇,我简直不敢相信,它像魔法一样运行。你救了我的一天。 - user1892203

4

Something like:

getInitialState: function() {
    return {
        something: { x: 0, y: 0 },
        blah: 10
    };
}

var state = Object.assign(this.state, {
    something: Object.assign(this.state.something, { y: 50 }),
});

this.setState(state);

如果使用递归/深度而不是硬编码树,效果会更好,但我将把这个决定留给读者 :)


外部的 Object.assign 实际上并没有做任何事情,因为你分配的 something 属性仍然指向原始对象(尽管已经改变)。此外,你不应该直接改变状态中的对象。虽然你调用了 setState,但这并不是一个干净的方法。此外,你可以向 setState 提供部分更新而不是完整的状态对象。 - herman
3
仔细思考后,可以得出一定要避免在改变this.state之后再使用this.state作为setState的参考,这样会导致在shouldComponentUpdate中比较下一个状态和之前状态时产生问题(因为两者是同一个对象的引用,所以始终相等)。 - herman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接