【问题标题】:Can not update state inside setInterval in react hook无法在反应钩子中更新 setInterval 内的状态
【发布时间】:2019-10-15 18:41:56
【问题描述】:

我想在 setinterval() 中每秒更新一次状态,但它不起作用。 我是反应钩子的新手,所以不明白为什么会这样。 请看下面的代码 sn-p 并给我建议。

// State definition

const [gamePlayTime, setGamePlayTime] = React.useState(100);
let targetShowTime = 3;
.........................
// call function
React.useEffect(() => {
    gameStart();
  }, []);
.............

const gameStart = () => {
    gameStartInternal = setInterval(() => {
      console.log(gamePlayTime); //always prints 100
      if (gamePlayTime % targetShowTime === 0) {

        //can not get inside here

        const random = (Math.floor(Math.random() * 10000) % wp("70")) + wp("10");
        const targetPosition = { x: random, y: hp("90") };
        const spinInfoData = getspinArray()[Math.floor(Math.random() * 10) % 4];
        NewSpinShow(targetPosition, spinInfoData, spinSpeed);
      }
      setGamePlayTime(gamePlayTime - 1);
    }, 1000);
  };

【问题讨论】:

  • 你在哪里调用了gameStart函数?
  • 感谢您的回复。我会更新我的问题。
  • 我刚刚用调用这个函数更新了我的问题。
  • 能否提供完整的组件代码。
  • @tomcarrier 是对的

标签: reactjs setinterval react-hooks


【解决方案1】:

你没有得到更新状态的原因是因为你在里面调用了它 useEffect(() => {}, []) 只被调用一次。

useEffect(() => {}, []) 就像 componentDidMount() 一样工作。

当调用 gameStart 函数时,gamePlaytime 为 100,在 gameStart 内部,它使用相同的值,但是计时器工作并且实际的 gamePlayTime 发生了变化。 在这种情况下,您应该使用 useEffect 监控 gamePlayTime 的变化。

...
  useEffect(() => {
      if (gamePlayTime % targetShowTime === 0) {
        const random = (Math.floor(Math.random() * 10000) % wp("70")) + wp("10");
        const targetPosition = { x: random, y: hp("90") };
        const spinInfoData = getspinArray()[Math.floor(Math.random() * 10) % 4];
        NewSpinShow(targetPosition, spinInfoData, spinSpeed);
      }
  }, [gamePlayTime]);

  const gameStart = () => {
    gameStartInternal = setInterval(() => {
      setGamePlayTime(t => t-1);
    }, 1000);
  };
...

【讨论】:

  • 感谢您的帮助,汤姆。现在对我来说效果很好。我明白我的错。
【解决方案2】:

您正在创建一个 closure,因为 gameStart() 在 useEffect 挂钩运行时“捕获”一次 gamePlayTime 的值,之后不再更新。

要解决这个问题,您必须使用 React 挂钩状态更新的 functional update pattern。不是直接将新值传递给setGamePlayTime(),而是传递一个函数,该函数在执行时接收旧状态值并返回一个新值进行更新。例如:

setGamePlayTime((oldValue) => {
  const someNewValue = oldValue + 1;
  return someNewValue;
});

试试这个(基本上只是用功能状态更新包装你的 setInterval 函数的内容):

const [gamePlayTime, setGamePlayTime] = React.useState(100);
let targetShowTime = 3;

// call function
React.useEffect(() => {
    gameStart();
  }, []);

const gameStart = () => {
    gameStartInternal = setInterval(() => {
      setGamePlayTime((oldGamePlayTime) => {
        console.log(oldGamePlayTime); // will print previous gamePlayTime value
        if (oldGamePlayTime % targetShowTime === 0) {
          const random = (Math.floor(Math.random() * 10000) % wp("70")) + wp("10");
          const targetPosition = { x: random, y: hp("90") };
          const spinInfoData = getspinArray()[Math.floor(Math.random() * 10) % 4];
          NewSpinShow(targetPosition, spinInfoData, spinSpeed);
        }
        return oldGamePlayTime - 1;
      });
    }, 1000);
  };

【讨论】:

  • 只是为了强调:这有效但不是因为功能版本以某种方式克服(穿透?)闭包,而是因为useState返回的setter在每个渲染上都是引用相等的(否则它会更新旧状态但是什么都不会改变)
【解决方案3】:

您不应该将 setInterval 与钩子一起使用。看看 React.js 的维护者之一 Dan Abramov 在他的博客上就替代方案所说的话:https://overreacted.io/making-setinterval-declarative-with-react-hooks/

【讨论】:

  • 不知道为什么这值得一票否决,但好吧。
【解决方案4】:

https://overreacted.io/making-setinterval-declarative-with-react-hooks/

Dan Abramov 文章很好地解释了如何使用钩子、状态和 API 的 setInterval() 类型!

丹·阿布拉莫夫!是React 维护团队的一员!众所周知,我个人爱他!

快速解释

问题是如何使用仅执行一次(第一次渲染)的 useEffect() 访问状态的问题!

简短的回答是:通过使用引用(useRef)!另一个 useEffect() 在需要更新时再次运行!或者在每次渲染时!

让我解释一下!并检查 Dan Abramov 解决方案!最后你会得到更好的上面的陈述!第二个例子与 setInterval() 无关!

=>

useEffect() 要么只运行一次,要么在每次渲染中运行!或者当依赖更新时(提供时)!

只有通过在每个相关时间运行和渲染的 useEffect() 才能访问状态!

或通过setState((state/*here the state*/) => <newStateExpression>)

但如果你想访问 useEffect() => 中的状态,则需要重新运行!意思是传递和执行新的回调!

这不适用于 setInterval!如果你每次都设置它!计数器重置!如果组件快速重新渲染,将导致不执行!

如果你只渲染一次!状态未更新!作为第一次运行,运行一个回调!并关闭!状态是固定的! useEffect(() => { <run once, state will stay the same> setInterval(() => { <state fixed> }) }, []).

对于所有这种情况!我们需要使用 useRef! (refs)!

保存一个保存状态的回调!来自每次重新渲染的 useEffect() !或者通过将状态值本身保存在 ref 中!视用途而定!

setInterval 的 Dan abramov 解决方案(简单而干净)

这就是你要找的东西!

useInteval 钩子(作者 Dan Abramov)

import React, { useState, useEffect, useRef } from 'react';

function useInterval(callback, delay) {
  const savedCallback = useRef();

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

用法

import React, { useState, useEffect, useRef } from 'react';

function Counter() {
  let [count, setCount] = useState(0);

  useInterval(() => {
    // Your custom logic here
    setCount(count + 1);
  }, 1000);

  return <h1>{count}</h1>;
}

我们可以看到他如何在每次重新渲染时保存新的回调!包含新状态的回调!

使用!这是一个干净简单的钩子!好美啊!

请务必阅读 Dan 的文章!正如他解释并解决了很多事情!

setState()

Dan Abramov 在他的文章中提到了这一点!

如果我们需要设置状态!在一个 setInteral 内!可以简单地将 setState() 与回调版本一起使用!

useState(() => {
   setInterval(() => {
      setState((state/*we have the latest state*/) => {
          // read and use state
          return <newStateExpression>;
      })
   }, 1000);
}, []) // run only once

我们甚至可以使用它!即使我们没有设置状态!可能的!虽然不好!我们只是返回相同的状态值!

setState((state) => {
   // Run right away!
   // access latest state
   return state; // same value (state didn't change)
});

但是这会使不同的反应内部代码部分运行(1,2,3),并检查!最终摆脱重新渲染!很高兴知道!

我们仅在更新状态时使用它!如果不 !那我们就需要用到refs了!

另一个例子:使用 getter 版本的 useState()

展示如何使用 refs 和状态访问!让我们再举一个例子!这里是另一种模式!在回调中传递状态!

import React from 'react';

function useState(defaultVal) {
    // getting the state
    const [state, setState] = React.useState(defaultValue);
    // state holding ref
    const stateRef = React.useRef();
    stateRef.current = state; // setting directly here!
    // Because we need to return things at the end of the hook execution
    // not an effect

    // getter
    function getState() {
       // returning the ref (not the state directly) 
       // So getter can be used any where!
       return stateRef.current;
    } 
    
    return [state, setState, getState];
}

示例属于同一类!但是这里没有效果!

但是我们可以使用上面的钩子来访问钩子中的状态,如下所示!

const [state, useState, getState] = useState(); // Our version! not react
// ref is already uptated by this call

React.useEffect(() => {
  setInteval(() => {
      const state = getState();
      // do what you want with the state!
      // it works because of the ref! Get State return a value to the same ref!
     // which is already updated 
  }, 1000)
}, []); // running only once

对于 setInterval()!好的解决方案是 Dan Abramov 钩子!为一个东西制作一个强大的自定义钩子是一件很酷的事情!第二个例子更多地展示了 refs 的用法和重要性,在这种状态下访问需求或问题!

很简单!我们总是可以制作自定义钩子!使用参考!并更新 ref 中的状态!或者一个保持新状态的回调!视使用情况而定!我们在渲染上设置 ref(直接在自定义钩子中 [render() 中的块执行])!或者在 useEffect() 中!在每次渲染时重新运行或根据依赖项重新运行!

关于 useEffect() 和 refs 设置的注意事项

关于useEffect()

的注意事项

useEffect => useEffect 在渲染被绘制到屏幕后异步运行。

  • 您以某种方式导致渲染(更改状态或父级重新渲染)
  • React 呈现您的组件(调用它)
  • 屏幕视觉更新
  • 然后 useEffect 运行

一件很重要的事情! useEffect() 在 render() 完成后运行,屏幕视觉更新!最后运行!你应该知道!

不过一般! 效果应该在 useEffect() 上运行!所以任何自定义钩子都可以!因为它的 useEffect() 将在绘制之后运行,在渲染 useEffect() 之前运行!如果不!就像需要直接在渲染中运行一样!那么你应该直接传递状态!有些人可能会通过回调!想象一些逻辑组件!并传递给它的 getState 回调!不是一个好习惯!
如果你在某种意义上做某事!谈论参考!确保正确更新参考!之前!

但通常你永远不会有问题!如果你这样做,那就是一种气味!您尝试的方式很高可能不是正确的好方法!

总的来说,我希望这能给你一个很好的感觉!

【讨论】:

    猜你喜欢
    • 2020-02-12
    • 2019-08-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-19
    • 2021-05-19
    • 1970-01-01
    相关资源
    最近更新 更多