React.js基于其他状态的状态

38

我在使用React.js时遇到了一些问题,即在调用setState()时状态不能立即设置。我不确定是否有更好的方法来解决这个问题,或者它是否真的只是React的一个缺陷。我有两个状态变量,其中一个基于另一个变量。(原始问题的演示: http://jsfiddle.net/kb3gN/4415/ 你可以在日志中看到当你点击按钮时,状态并没有立即设置)

setAlarmTime: function(time) {
  this.setState({ alarmTime: time });
  this.checkAlarm();
},
checkAlarm: function() {
  this.setState({
    alarmSet: this.state.alarmTime > 0 && this.state.elapsedTime < this.state.alarmTime
  });
}, ...

在调用setAlarmTime时,由于this.state.alarmTime不会立即更新,因此对checkAlarm的紧随其后的调用基于this.state.alarmTime的先前值进行设置,因此是不正确的。

我通过将checkAlarm的调用移动到setAlarmTimesetState回调中来解决这个问题,但必须跟踪哪个状态实际上是“正确”的,并尝试将所有内容都适合回调似乎很荒谬:

setAlarmTime: function(time) {
  this.setState({ alarmTime: time }, this.checkAlarm);
}

有更好的方法吗?我的代码中还有几个地方引用了我刚设置的状态,现在我不确定何时可以信任这个状态!

谢谢

3个回答

33

是的,setState 是异步的,所以 this.state 不会立即更新。以下是批处理的单元测试,其中可能解释了一些细节。

在上面的示例中,alarmSet 是从 alarmTimeelapsedTime 状态计算出的数据。一般来说,计算数据不应存储在对象的状态中,而应作为呈现方法的一部分根据需要进行计算。在互动和动态 UI 文档底部有一个什么不应该放在状态中?部分,其中列举了不应该放在状态中的类似这样的事情,组件应该具有状态吗? 部分解释了这样做的一些原因。


在大多数情况下,将状态尽可能简化并在渲染函数中重新计算事物是有意义的,因为此时this.state将完全更新。但是,在我的情况下,我认为这不起作用,因为该组件实际上具有自己的更新循环(使用setInterval),必须在每个tick中更新和检查其状态。因此,不能保证在tick之间运行render。为了解决这个问题,我实现了一个包装器React.createClass来跟踪它自己的状态。我也会将它添加到答案中http://jsfiddle.net/kb3gN/4448/ - Andrew
2
这里有一篇关于ReactJS中Flux架构的好文章:http://facebook.github.io/react/docs/flux-overview.html。他们做的其中一件事是将“复杂”的状态从视图层的React组件中分离出来,这也可以适用于这里。不要将setInterval放在视图中,而应该放在存储器中,每当存储器更新时,它会触发视图重新渲染。存储器可以是普通的JavaScript对象,没有setState的异步行为。 - Douglas
感谢您的解释。 - ksharifbd
想知道是否将依赖状态移动到 useEffect 中是一个好主意,这样种子状态的更改将触发其他所有内容的更新。 - piyush tripathi
我的理解是:计算出来的数据不应该存储在对象的状态中! - emarshah

4
正如Douglas所说,将计算状态保存在`this.state`中通常不是一个好主意,而是在组件的`render`函数中每次重新计算,因为此时状态已经被更新。
然而,这对我来说行不通,因为我的组件实际上有自己的更新循环,需要在每个tick检查并可能更新其状态。由于我们不能保证在每个tick时`this.state`都已经更新,所以我创建了一个解决方法,它包装了`React.createClass`并添加了自己的内部状态跟踪。(需要jQuery的`$.extend`)(Fiddle:http://jsfiddle.net/kb3gN/4448/
var Utils = new function() {
  this.createClass = function(object) {
    return React.createClass(
      $.extend(
        object,
        {
          _state: object.getInitialState ? object.getInitialState() : {},
          _setState: function(newState) {
            $.extend(this._state, newState);
            this.setState(newState);
          }
        }
      )
    );
  }
}

对于需要在渲染函数之外获取最新状态的任何组件,只需将调用React.createClass替换为Utils.createClass

您还需要将所有this.setState调用更改为this._setState,并将this.state调用更改为this._state

这样做的最后一个后果是,您将失去组件中自动生成的displayName属性。这是由jsx转换器将

var anotherComponent = React.createClass({

替换为

var anotherComponent = React.createClass({displayName:'anotherComponent'

要解决此问题,您只需手动添加displayName属性到您的对象中即可。

希望这可以帮助到您。


1
一种更简单的方法是将状态完全存储在React之外,并将其作为props传递给React - 然后您的状态始终是最新的,而React可能尚未协调,这并不重要。 - Dustin Getz

2

不确定在提问时是否是这种情况,但现在this.setState(stateChangeObject, callback)的第二个参数可以接受一个可选的回调函数,因此可以这样做:

setAlarmTime: function(time) {
  this.setState({ alarmTime: time }, this.checkAlarm);
},
checkAlarm: function() {
  this.setState({
    alarmSet: this.state.alarmTime > 0 && this.state.elapsedTime < this.state.alarmTime
  });
}, ...

2
你不应该在 this.setState(...) 中使用 this.state。因为 this.setState(...) 是异步的(大多数情况下),所以你无法预测执行时 this.state 的内容。正确的做法是:this.setState(prevState => ({ alarmSet: prevState.alarmTime > 0 && prevState.elapsedTime < prevState.alarmTime })); - chillyistkult

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