在这种情况下是否需要使用setState(function)重载? [英] Do I need to use setState(function) overload in this case?

查看:36
本文介绍了在这种情况下是否需要使用setState(function)重载?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

想象一下这样的情况:

clickHandler(event) {
  var that = this;
  var cellIndex = event.target.id;

  if(that.inProcess) return;

  /* Temporarily clone state */
  var cloneState = JSON.parse(JSON.stringify(this.state));

  var currentCell = cloneState.gameCellArray[cellIndex];
  currentCell.shown = true;

  if (that.someCondition) {

          that.inProcess = true;
          setTimeout(function () {

              currentCell.shown = false;
              that.setState(cloneState);
              that.inProcess = false;

          }, 1000)

  }
  this.setState(cloneState);

}

现在,我在理论上担心在我克隆状态( clickHandler 中的第四条语句)的地方,因为没有得到最新版本的状态-因为某些 setState之前调用 clickHandler 调用正在处理中,这是有效的假设吗?

Now I got worried in theory that where I clone state (fourth statement in clickHandler) it can happen that I don't get the most recent version of the state - since some setState calls from previous invocation of clickHandler are pending, is it valid assumption?

现在,我的问题是,如果我以以下方式(基本上使用带有回调参数的 setState )重写上述代码,会更安全吗?(就不处于不同步状态而言).

Now, my question is, if I rewrite above code in the following way (basically using setState with callback parameter), will it be safer? (in terms of not having out of synch state).

clickHandler(event) {
    var that = this;
    var cellIndex = event.target.id;
    if(that.inProcess) return;

    that.setState(function(prevState, props){

        var cloneState = JSON.parse(JSON.stringify(prevState));

        var currentCell = cloneState.gameCellArray[cellIndex];
        currentCell.shown = true;

        if (that.someCondition) {

                that.inProcess = true;
                setTimeout(function () {
                    currentCell.shown = false;
                    // Do I need to take some measures here also?
                    that.setState(cloneState);
                    that.inProcess = false;

                }, 1000)

        }
        return cloneState;

    })

}

我是否还需要在 setTimeout (第二个重写版本)内采取其他措施?

Do I need to take some additional measure inside setTimeout also (the second rewritten version)?

ps. clickHandler 是仅更改状态的函数-假设当用户单击某些按钮时被调用.

ps. clickHandler is only function which changes state - and let's say gets called when user clicks some buttons.

pps.更一般而言,从我的第一种情况或第二种情况来看,从状态来看可能会出问题(从同步的角度来看),我们将详细说明其答案,以便更好地了解如何在反应中管理状态.

pps. More generally what could go wrong (from synchronization point of view) with the state in either my first case or second version, elaborate answer would be appreciated, in order to better understand how to manage state in react.

推荐答案

这是一个很长的答案.如果您不想全部阅读,请向下滚动至底部的TLDR.

请注意,我描述了一些可能在React 17+中更改的实现细节.这就是为什么我们的文档稍微模糊一些的原因,以便大多数人不会过多地依赖实施细节.但是在这种情况下,您似乎对真正的工作方式特别感兴趣,所以我必须比我想要的更加具体.

Note that I describe some implementation details that could change in React 17+. This is why our docs are a little more vague so that most people don’t rely on implementation details too much. But in this case it seems like you’re specifically interested in how it really works so I have to be more specific than I’d like to be.

现在,我在理论上担心在我克隆状态(clickHandler中的第三条语句)的地方,可能会发生我没有获得该状态的最新版本的情况-因为来自上一次clickHandler调用的一些setState调用正在等待处理,有效的假设吗?

Now I got worried in theory that where I clone state (third statement in clickHandler) it can happen that I don't get the most recent version of the state - since some setState calls from previous invocation of clickHandler are pending, is it valid assumption?

不.在回复时(反应16和任何早期版本),事件处理程序中的 this.state 可以安全地读取,然后再自行更新状态.所以这段代码很好:

No. At the time of this reply (React 16 and any earlier versions), this.state in the event handler is safe to read before you update the state yourself. So this code is fine:

handleClick() {
  var something = this.state.something;

它将为您提供当前状态.

It will give you the current state.

唯一的陷阱是,如果您自己调用 setState ,则不应期望 this.state 立即更新.所以这段代码行不通:

The only pitfall is that if you call setState yourself, you shouldn’t expect this.state to be updated immediately. So this code wouldn’t work:

handleClick(e) {
  this.setState({ something: e.target.value });
  var something = this.state.something; // Don't expect to get newValue here

注意:还有另一种极端情况在注释中指出:如果您有多个 onClick 处理程序,则存在相同的陷阱:在子事件处理程序中调用 setState()后,您可以不能依赖在父事件处理程序运行时更新 this.state .实际上,这就是这种优化如此有用的原因:来自单个浏览器事件的所有 setState()调用都将被分批处理,无论它们是在事件冒泡时发生在一个组件中还是在不同组件中发生.

Note: there is also another edge case pointed out in the comments: if you have several onClick handlers, the same pitfall applies: once you call setState() in a child event handler, you can’t rely on this.state being updated by the time the parent event handler runs. In fact it’s the reason this optimization is so useful: all setState() calls from a single browser event are batched, whether they happen in one or different components while the event bubbles.

仍然,这不是问题,因为如果您调用 setState ,您已经知道将其设置为什么:

Still, it isn’t a problem because if you called setState you already know what you set it to:

handleClick(e) {
  var newValue = e.target.value;
  this.setState({ something: newValue });
  // There's no need to "read" something from state.
  // Since you just set it, you already *know*
  // what you set it to. For example:
  doSomethingWith(newValue);

现在,有些情况下您想根据以前的状态更新状态.虽然您可以只在事件处理程序中读取 this.state ,但这只能运行一次:

Now, there are cases when you want to update state based on the previous state. While you could have just read this.state in the event handler, this only works once:

handleIncrement() {
  // This will increment once:
  this.setState({ counter: this.state.counter + 1 });
  // These won't work because this.state.counter isn't updated yet:
  this.setState({ counter: this.state.counter + 1 });
  this.setState({ counter: this.state.counter + 1 });

为了使您从不必担心此类情况中解放出来, React提供了另一个接受功能的 setState()重载.该功能将在应用更新时接收当前状态,因此您可以安全地使用它.React将确保通过所有未决函数线程化"当前状态:

To free you up from ever worrying about cases like this, React offers a different setState() overload that accepts a function. That function will receive the current state at the time the update is applied so you could safely use it. React will make sure to "thread" the current state through all the pending functions:

function increment(prevState) {
  return { counter: prevState.counter + 1 };
}

// ...
handleIncrement() {
  // Each function in the queue will receive the right state:
  this.setState(increment);
  this.setState(increment);
  this.setState(increment);
  // So this will increment three times.

从React 16及更早版本开始,此重载仅在您从同一事件处理程序多次调用 setState()时有用.但是,由于它也可以在其他情况下使用,因此我们通常建议您在 setState()调用依赖于当前状态的任何时候使用它,这样您就无需考虑关于这一切.但是,如果您的代码在没有它的情况下可以工作,并且尝试对其进行重写,则会使其更加混乱,请暂时不要打扰.

As of React 16 and earlier, this overload is only useful when you call setState() multiple times from the same event handler. However, since it works in other cases too, we generally recommend using it any time your setState() call depends on the current state so that you don’t need to think about this at all. However if your code works without it, and attempts to rewrite it make it more confusing, don’t bother for now.

将来,我们可能还会在更多情况下使用它,但我们会在将来的发行版中明确指出此类更改.我们还将为此开发一个更加自然"的API,因为我们注意到人们对矛盾感到困惑,因为 setState()的明显命令性以及我们建议的更实用的方法.

In the future we might also rely on it in more cases, but we’ll clearly call out any such changes in future releases. We will also be working on a more "natural" API for this since we’ve noticed people get confused by the contradiction because apparent imperative nature of setState(), and the more functional approach we recommend.

在您的特定情况下,我实际上认为第一种方法更简单.您只需在事件处理程序中调用一次 setState()(超时将在以后发生),因此多个连续调用的陷阱就不适用了.

In your particular case, I actually think the first approach is simpler. You only call setState() once in your event handler (timeout happens later), so the pitfall about multiple consecutive calls doesn’t apply.

您使用功能性 setState()形式的第二种方法实际上并未正确使用它,从而使整个代码更加混乱. setState()的功能形式假定您传递给它的函数为

Your second approach that uses the functional setState() form actually doesn’t use it correctly, making the overall code more confusing. The functional form of setState() presumes that the function you pass to it is pure. For example, this is a pure function:

function increment(prevState) {
  return { counter: prevState.counter + 1 };
}

但是,您传递的函数不仅会计算下一个状态,而且还会安排超时,保留一部分状态,对其进行适当的更改,然后在超时内再次调用 setState .显然,这不是不是纯函数的行为方式.经验法则是如果您不想在 render()内部执行任何操作,那么您也不应在 setState() updater函数内部执行该操作.

However, the function you pass not just calculates the next state, but also schedules a timeout, holds onto a piece of the state, mutates it in place, and inside the timeout calls setState again. This clearly is not how a pure function behaves. The rule of thumb is if you wouldn’t do something inside render(), you also shouldn’t do that inside the setState() updater function.

同样,在React 16或更少的版本中,在这种特殊情况下将代码重写为函数形式是没有好处的(我解释了上面的原因:您只需调用一次 setState(),并且您不尝试在其后立即读取状态).但是,如果您确实想使用函数形式,则需要确保传递的函数是纯函数.问题是:那么您将超时逻辑放在哪里?

Again, in React 16 or less, rewriting your code to the functional form in this particular case wouldn’t be beneficial (I explained the reasons above: you’re just calling setState() once, and you’re not trying to read the state right after it). But if you do want to use the functional form, you need to make sure the function you pass is pure. The question is: where do you put the timeout logic, then?

我认为超时逻辑最好放在 componentDidUpdate()生命周期挂钩中.这样,只要满足必要条件,状态更改就会真正触发它(无论组件在何处发生).例如,即使您有两个触发相同状态更改的按钮,它们都将导致 componentDidUpdate()触发,并且它可能会根据 how 来运行超时逻辑状态已更改.

My opinion is that the timeout logic would be better placed in the componentDidUpdate() lifecycle hook. This way it would be truly triggered by a state change—no matter where in the component it happened—as long as it satisfied the necessary conditions. For example, even if you had two buttons triggering the same state changes, they would both cause componentDidUpdate() to fire, and it could run the timeout logic depending on how the state changed.

由于您的问题是有关基于此GitHub讨论的实施记忆游戏的,我写了一些伪代码,说明我将如何处理此任务.让我在这里引用我的答案:

Since your question was about implementing a memory game, based on this GitHub discussion, I wrote some pseudocode on how I would approach this task instead. Let me quote my answer here:

我认为,如果将此逻辑的与超时相关的部分拆分为 componentDidUpdate 生命周期挂钩,则代码可能更易于理解.可能还有更好的方法来对状态本身进行建模.匹配的游戏看起来像是一个状态机",具有几个不同的有效状态(未选择任何内容,选择了一项并等待,选择了两个正确的项目,选择了两个错误的项目).

I think that if you split the timeout-related part of this logic into the componentDidUpdate lifecycle hook, the code could be easier to understand. There might also be a better way to model the state itself. The matching game seems like a "state machine" with a few different valid states (nothing selected, one item selected and waiting, two right items selected, two wrong items selected).

可能有必要将这些可能的游戏状态更直接地编码到您的组件状态中,并更仔细地考虑如何用对象表示它们.例如,可能不是一个单元格值数组,而是更容易考虑如下显式状态:

It might be worth encoding these possible game states more directly into your component state and think more carefully about how to represent them with objects. For example, it might be that instead of an array of cell values it is easier to think about an explicit state like:

{
  openedCells: [1, 2], // array of ids
  firstSelectedCell: 5, // could be null
  secondSelectedCell: 7, // could be null
}

,然后在 componentDidUpdate 中实现条件逻辑,例如

and then implement conditional logic in componentDidUpdate, e.g.

handleClick(e) {
  // Are we waiting for a timeout? Reset it.
  if (this.resetTimeout) {
    clearTimeout(this.resetTimeout);
  }

  const id = ... // get it from target node, or bind event handler to ID in render()
  this.setState(prevState => {
    if (prevState.firstSelectedCell !== null && prevState.secondSelectedCell === null) {
      // There is just one selected cell. We clicked on the second one.
      return {
        secondSelectedCell: id
      };
    }
    // We are selecting the first cell
    // (either because we clicked to reset both or because none were selected).
    return {
      firstSelectedCell: id,
      secondSelectedCell: null
    };
}

componentDidUpdate(prevState) {
  if (prevState.secondSelectedCell !== this.state.secondSelectedCell) {
    // We just picked the second cell.
    if (isSamePicture(
      this.state.secondSelectedCell,
      this.state.firstSelectedCell
    ) {
      // Same picture! Keep them open.
      this.setState(prevState => {
         // Add them both to opened cells and reset.
         return {
           firstSelectedCell: null,
           secondSelectedCell: null,
           openedCells: [
             ...prevState.openedCells,
             prevState.firstSelectedCell,
             prevState.secondSelectedCell
           ]
         };
    } else {
      // Clear both in a second.
      this.resetTimeout = setTimeout(() => {
        this.setState({
          firstSelectedCell: null,
          secondSelectedCell: null,
        });
      }, 1000);
    }
}

然后,在render方法中,如果它们位于 openedCells 中或者它们是 firstSelectedCell secondSelectedCell 中,则可以显示单元格.

Then, in the render method, you can show cells if either they are in openedCells or they are firstSelectedCell or secondSelectedCell.

我希望这会有所帮助!总结一下,这是TLDR :

I hope this helps! To sum up, here is the TLDR:

  • 至少在React 16(或更早版本)中,在事件处理程序中的第一个 setState()调用之前读取 this.state 会为您提供当前状态.但是不要期望在 setState()之后立即更新.
  • setState()的功能重载可以防止这种陷阱,但是它要求传递的函数是纯函数.设置超时不是纯粹的 .
  • componentDidUpdate()生命周期钩子可能是设置依赖状态的超时的更好位置.
  • At least in React 16 (or earlier), reading this.state before the first setState() call in the event handler will give you the current state. But don’t expect it to be updated immediately after setState().
  • The functional overload of setState() protects against this pitfall, but it requires that the passed function is pure. Setting timeouts is not pure.
  • The componentDidUpdate() lifecycle hook might be a better place for setting timeouts that depend on the state.

这篇关于在这种情况下是否需要使用setState(function)重载?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆