我理解React教程和文档在no uncertain terms中警告说,状态不应该直接变异,并且所有内容都应该通过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
documentation下的注释基本上确定了两个陷阱:
this.setState
,这可能会替换(覆盖?)你所做的突变。我不知道在上面的代码中会发生这种情况。setState
可能会以异步/延迟方式有效地改变this.state
,因此在调用this.state
后立即访问this.setState
时,您无法保证访问最终的变异状态。我明白了,如果this.setState
是更新函数的最后一次调用,那么这不是问题。答案 0 :(得分:31)
React跟随Unidirectional Data Flow。这意味着,反应中的数据流应该并且预计将处于循环路径中。
React的无通量数据流
为了使React像这样工作,开发人员使React类似于函数式编程。函数式编程的拇指规则是不变性。让我大声清楚地解释一下。
单向流如何工作?
states
是一个包含组件数据的数据存储。view
根据状态呈现。view
需要更改屏幕上的内容时,应该从store
提供该值。setState()
函数,该函数接收object
新states
并执行比较和合并(类似于object.assign()
) state并将新状态添加到状态数据存储。view
消耗的新状态重新呈现并在scree上显示它。此循环将在整个组件的整个生命周期内继续。
如果您看到上述步骤,则会清楚地显示更改状态时发生的很多事情。因此,当您直接改变状态并使用空对象调用setState()
时。 previous state
将被您的突变污染。由于这个原因,两个状态的浅比较和合并将会受到干扰或不会发生,因为现在你只有一个状态。这将破坏React的所有生命周期方法。
因此,您的应用会出现异常甚至崩溃。大多数情况下,它不会影响您的应用,因为我们用于测试它的所有应用都非常小。
JavaScript中Objects
和Arrays
变异的另一个缺点是,当您分配对象或数组时,您只是引用该对象或该数组。当你改变它们时,对该对象或那个数组的所有引用都将受到影响。 React在后台以智能方式处理它,并简单地为我们提供API以使其工作。
处理反应中的状态时最常见的错误
//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的美丽,它会让你成为一个糟糕的程序员。
那么,处理反应状态的最佳方法是什么?让我解释一下。
当您需要在现有状态下更改“某事物”时,首先从当前状态获取该“某事物”的副本。
//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
。这使您可以控制代码,这将帮助您编写优雅的测试,并使您对生产中的代码性能充满信心。
要回答你的问题,
为什么我不能直接修改组件的状态?
是的,你可以。但是,你需要面对以下后果。
state
的控制权。PS。我写了大约10000行可变的React js代码。如果它现在中断了,我不知道在哪里查看,因为所有的值都在某处变异。当我意识到这一点时,我开始编写不可变代码。相信我!对产品或应用程序来说,这是最好的选择。
希望这有帮助!
答案 1 :(得分:24)
The React docs for 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的交互方式,以至于这些覆盖或其他问题都无法发生,但是您正在创建一种情况,其他开发人员或未来的更新可能会突然发现自己的奇怪或微妙的问题。
答案 2 :(得分:1)
为避免每次都创建this.state.element
的副本,可以将update与$set or $push
或immutability-helper中的许多其他对象一起使用
例如:
import update from 'immutability-helper';
const newData = update(myData, {
x: {y: {z: {$set: 7}}},
a: {b: {$push: [9]}}
});
答案 3 :(得分:1)
setState触发组件的重新呈现。当我们想要一次又一次地更新状态时,我们必须需要setState,否则它将无法正常工作。
答案 4 :(得分:1)
令我惊讶的是,当前答案中没有一个提到纯/记忆成分。仅当检测到其中一个道具发生变化时,这些组件才会重新渲染。
假设您直接更改状态并传递,而不是传递值,而是将过度耦合的对象传递给下面的组件。该对象仍具有与上一个对象相同的引用,这意味着即使您对其中一个属性进行了突变,pure / memo组件也不会重新呈现。
由于从库中导入组件时,您并不总是知道您正在使用哪种类型的组件,这是坚持不变规则的另一个原因。
以下是此行为的示例(使用R.evolve
简化创建副本并更新嵌套内容):
class App extends React.Component {
state = {some: {rather: {deeply: {nested: {stuff: 1}}}}};
mutatingIncrement = () => {
this.state.some.rather.deeply.nested.stuff++;
this.setState({});
}
nonMutatingIncrement = () => {
this.setState(R.evolve(
{some: {rather: {deeply: {nested: {stuff: n => n + 1}}}}}
));
}
render() {
return <div>
Pure Component: <PureCounterDisplay {...this.state} />
<br />
Normal Component: <CounterDisplay {...this.state} />
<br />
<button onClick={this.mutatingIncrement}>mutating increment</button>
<button onClick={this.nonMutatingIncrement}>non-mutating increment</button>
</div>;
}
}
const CounterDisplay = (props) => (
<React.Fragment>
Counter value: {props.some.rather.deeply.nested.stuff}
</React.Fragment>
);
const PureCounterDisplay = React.memo(CounterDisplay);
ReactDOM.render(<App />, document.querySelector("#root"));
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/ramda@0/dist/ramda.min.js"></script>
<div id="root"></div>
答案 5 :(得分:0)
“
的最简单答案为什么我不能直接修改组件的状态:
都是关于更新阶段。
当我们更新组件的状态时,它的所有子组件也将被渲染。或渲染的整个组件树。
但是当我说我们渲染了整个组件树时,这并不意味着整个DOM都会更新。 渲染组件时,我们基本上会得到一个react元素,从而可以更新虚拟dom。
然后,React将查看虚拟DOM,它还具有旧虚拟DOM的副本,这就是为什么我们不应该直接更新状态的原因,因此我们可以有两个不同的对象引用在内存中,我们拥有旧的虚拟DOM和新的虚拟DOM。
然后react将找出更改的内容,并在此基础上相应地更新实际DOM。
希望有帮助。
答案 6 :(得分:-1)