无法在反应挂钩中的setInterval内部更新状态

时间:2019-10-15 18:41:56

标签: reactjs setinterval react-hooks

我想在setinterval()中每秒更新一次状态,但是它不起作用。 我是刚接触钩子的新手,所以无法理解为什么会这样。 请看下面的代码片段,并给我建议。

// 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);
  };

4 个答案:

答案 0 :(得分:3)

之所以没有获得更新状态,是因为您在内部调用了它 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);
  };
...

答案 1 :(得分:2)

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

要解决此问题,必须使用React钩子状态更新的functional update pattern。与其直接向setGamePlayTime()传递新值,不如将其传递给 function ,该函数在执行时会接收旧状态值并返回新值以进行更新。例如:

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);
  };

答案 2 :(得分:1)

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

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

丹·阿布拉莫夫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 中保存状态值本身!视用途而定!

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

这就是你要找的!

useInteval 钩子(丹·阿布拉莫夫)

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>;
}

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

使用!这是一个干净简单的钩子!太美了!

请务必阅读丹的文章!正如他解释和解决了很多事情一样!

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)
});

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

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

另一个例子:useState() 和 getter 版本

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

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() 完成后运行,屏幕在视觉上更新!最后运行!你应该知道!

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

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

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

答案 3 :(得分:0)

您不应将setInterval与钩子一起使用。看看React.js的维护者之一丹·阿布拉莫夫(Dan Abramov)关于他博客中的另一种说法:https://overreacted.io/making-setinterval-declarative-with-react-hooks/