我为什么不能直接修改组件的状态?


78

我了解React教程和文档会发出警告 毫无疑问地说,状态不应该直接变异,所有事情都应该通过setState

我想了解为什么,我不能直接更改状态,然后(在同一函数中)调用 this.setState({})仅来触发它render

例如:以下代码似乎正常工作:

const React = require('react');

const App = React.createClass({
    getInitialState: function() {
        return {
            some: {
                rather: {
                    deeply: {
                        embedded: {
                            stuff: 1
                        }}}}};
    },
    updateCounter: function () {
        this.state.some.rather.deeply.embedded.stuff++;
        this.setState({}); // just to trigger the render ...
    },
    render: function() {
        return (
                <div>
                Counter value: {this.state.some.rather.deeply.embedded.stuff}
                <br></br>
                <button onClick={this.updateCounter}>inc</button>
                </div>
        );
    }
});

export default App;

我全都遵循以下约定,但我想进一步加深对ReactJS实际工作方式的理解,以及可能出现问题的地方,或者上述代码不是最佳选择。

文档下的注释this.setState基本上标识了两个陷阱:

  1. 如果您直接更改状态然后再调用 this.setState它,则可能会替换(覆盖?)所做的更改。我看不到上面的代码中如何发生这种情况。
  2. setState可能会this.state以异步/延迟的方式有效地进行更改,因此,this.state在调用后this.setState立即访问时,不能保证您访问最终的更改状态。我知道,如果this.setState更新功能的最后一次调用不是问题,

2
检查文档中注释setState。它涵盖了一些很好的理由。
Yuya

@Kujira我更新了我的问题,以解决您提到的笔记。
Marcus Junius Brutus

4
除了您认为自己可以控制它之外,您还只是在缩短框架的工作流程。使用Javascript,您可以做到这一点,只需记住,一旦打破模式,框架就不再对状态的一致性负责。
pietro909

4
这不是“不能”直接改变状态的问题,而是“不应该”的问题。
rossipedia

奇怪,这是4个月前问过的,但仍然没有被接受的答案,这很难回答吗?我真的无法使用Google找到解决方案……
jhegedus

Answers:


47

React文档setState有这样的说法:

切勿this.state直接突变,因为setState()随后的呼叫可能会取代您所做的突变。对待this.state就好像它是不可变的。

setState()不会立即变异,this.state但会创建待处理的状态转换。this.state调用此方法后进行访问可能会返回现有值。

不能保证对呼叫的同步操作,setState并且可以为提高性能而对呼叫进行批量处理。

setState() 除非在中实现了条件渲染逻辑,否则它将始终触发重新渲染 shouldComponentUpdate()。如果正在使用可变对象,并且无法在中实现逻辑shouldComponentUpdate(),则setState()仅在新状态与先前状态不同时进行调用才能避免不必要的重新渲染。

基本上,如果您修改 this.state直接进行,则会造成一种情况,即这些修改可能会被覆盖。

与您的扩展问题1)和2)有关,setState()不是立即的。它将根据其认为正在进行的状态进行排队,其中可能不包括对的直接更改this.state。由于已排队而不是立即应用,因此完全有可能在两者之间进行某些修改,以使您的直接更改被覆盖。

如果没有别的,您可能会觉得更好,只是考虑将不直接修改this.state视为良好做法。您可能个人知道您的代码与React交互时不会发生这些覆盖或其他问题,但是您正在创建一种情况,其他开发人员或将来的更新可能突然发现自己遇到奇怪或微妙的问题。


“它根据其正在进行的状态排队状态转换,其中可能不包括对this.state的直接更改。由于已排队而不是立即应用,因此完全有可能在两者之间进行某些修改,以使您的直接更改被覆盖。” 您能详细说明一下这是什么意思吗?举一个有问题的例子吗?
jhegedus '16

1
@jhegedus深入研究React代码库:处理状态更改非常详细地介绍了setState()and回调系统如何工作,并让您清楚了为什么会出现问题。
Ouroborus

感谢小费Ouroborus!
jhegedus

因以下原因而被投票:“它根据其正在进行的状态排队进行状态转换,其中可能不包括对this.state的直接更改”
Marcus Junius Brutus

56

这个答案是要提供足够的信息,以免直接在React中更改/更改状态。

React遵循单向数据流。意思是,react内部的数据流应该并且应该处于循环路径中。

无流量的React数据流

在此处输入图片说明

为了使React像这样工作,开发人员使React类似于函数式编程。函数式编程的经验法则是不变性。让我大声说清楚。

单向流如何工作?

  • states 是包含组件数据的数据存储。
  • 所述view一个部件的渲染基于状态。
  • view需要在屏幕上更改某些内容时,应从中提供该值store
  • 为了实现这一点,React提供setState()了一个函数,该函数接受objectnew的值statesobject.assign()并对先前状态进行比较和合并(类似于),并将新状态添加到状态数据存储中。
  • 每当状态存储中的数据发生更改时,react就会触发新的状态重新渲染,view消耗并显示在屏幕上。

该周期将在组件的整个生命周期中继续。

如果您看到上述步骤,则可以清楚地看到更改状态后发生了很多事情。因此,当您直接更改状态并setState()使用空对象进行调用时。该previous state会与你的基因突变被污染。因此,两个状态的浅表比较和合并将被干扰或不会发生,因为您现在只有一个状态。这将破坏所有React的生命周期方法。

结果,您的应用程序将表现异常甚至崩溃。在大多数情况下,它不会影响您的应用程序,因为我们用于测试此应用程序的所有应用程序都非常小。

和突变的另一个缺点ObjectsArrays在JavaScript中,当您分配一个对象或数组,你要做的仅仅是该对象或数组的一个参考。对它们进行突变时,对该对象或该数组的所有引用都会受到影响。React在后台以一种智能的方式处理此问题,只需给我们提供一个API即可使其工作。

在React中处理状态时完成的最常见错误

// original state
this.state = {
  a: [1,2,3,4,5]
}

// changing the state in react
// need to add '6' in the array

// bad approach
const b = this.state.a.push(6)
this.setState({
  a: b
}) 

在上面的示例中,this.state.a.push(6)将直接改变状态。将其分配给另一个变量并调用setState与下面显示的相同。无论如何,当我们改变状态时,没有必要将其分配给另一个变量并setState使用该变量进行调用。

// same as 
this.state.a.push(6)
this.setState({})

大多数人都这样做。错了。这打破了React的美丽,并且会让您成为一个不好的程序员。

那么,在React中处理状态的最佳方法是什么?让我解释。

当您需要在现有状态下更改“内容”时,请先从当前状态获取该“内容”的副本。

// original state
this.state = {
  a: [1,2,3,4,5]
}

// changing the state in react
// need to add '6' in the array

// create a copy of this.state.a
// you can use ES6's destructuring or loadash's _.clone()
const currentStateCopy = [...this.state.a]

现在,更改currentStateCopy不会更改原始状态。进行操作,currentStateCopy然后使用将其设置为新状态setState()

currentStateCopy.push(6)
this.state({
  a: currentStateCopy
})

这很漂亮吧​​?

这样一来,所有的引用this.state.a都不会受到影响,直到我们使用setState。这使您可以控制代码,并且可以帮助您编写优雅的测试,并使您对生产中的代码性能充满信心。

为了回答您的问题,

为什么我不能直接修改组件的状态?


是的,您可以。但是,您需要面对以下后果。

  1. 扩展时,您将编写难以管理的代码。
  2. 您将失去对state所有组件的控制。
  3. 您将使用React编写自定义代码,而不是使用React。

不变性不是必需的,因为JavaScript是单线程的。但是,遵循实践会很好,从长远来看会有所帮助。

PS。我已经写了大约10000行可变的React JS代码。如果现在中断了,我不知道去哪里寻找,因为所有值都在某个地方发生了突变。当我意识到这一点时,我开始编写不可变的代码。相信我!这是对产品或应用程序所做的最好的事情。

希望这可以帮助!


2
提醒一下:JS中最基本的克隆方法(slice,ES6解构等)很浅。如果您有嵌套数组或嵌套对象,则需要查看其他深度复制方法,例如JSON.parse(JSON.stringify(obj))(尽管如果您的对象具有循环引用,则此特定方法将不起作用)。
加伦·朗

2
@Galen Long就_.cloneDeep足够了
Josh Broadhurst

2
@JoshBroadhurst很好,是的。而且,如果有人担心只为一个功能安装整个Lodash软件包,则可以仅将cloneDeep模块与一起安装,npm install lodash.clonedeep并与一起使用let cloneDeep = require("lodash.clonedeep");
加伦·朗

5

对“

为什么我不能直接修改组件的状态:

更新阶段有关。

当我们更新组件的状态时,它的所有子组件也将被渲染。或渲染的整个组件树。

但是当我说我们的整个组件树被渲染时,这并不意味着整个DOM都会被更新。当渲染一个组件时,我们基本上得到了一个react元素,因此可以更新我们的虚拟dom。

然后,React将查看虚拟DOM,它还具有旧虚拟DOM的副本,这就是为什么我们不应该直接更新状态的原因,因此我们可以在内存中有两个不同的对象引用,我们拥有旧的虚拟DOM作为以及新的虚拟DOM。

然后react将找出改变的内容,并在此基础上相应地更新真实的DOM。

希望能帮助到你。


1

为了避免每次都创建一个副本,this.state.element可以使用update with$set or $pushimmutability-helper中的许多其他副本

例如:

import update from 'immutability-helper';

const newData = update(myData, {
  x: {y: {z: {$set: 7}}},
  a: {b: {$push: [9]}}
});

1

setState触发组件的重新呈现。当我们想一次又一次地更新状态时,我们必须需要setState,否则它将无法正常工作。


-3

我目前的理解是基于这个答案:

如果不使用 shouldComponentUpdate或任何其他生命周期方法(例如componentWillReceivePropscomponentWillUpdatecomponentDidUpdate),您比较新旧道具/状态

然后

可以先进行突变state然后调用setState(),否则是不正确的。


7
注意,这仅适用于当前和过去的React版本(当前在v15.3.2)。谁知道您的代码是否会在将来的版本中中断,因为它依赖于记录为被禁止的行为。
TomW
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.