【问题标题】:State not updating when using React state hook within setInterval在 setInterval 中使用 React 状态挂钩时状态未更新
【发布时间】:2019-04-01 03:28:57
【问题描述】:

我正在尝试新的React Hooks 并有一个带有计数器的时钟组件,该计数器应该每秒增加一次。但是,该值不会增加超过 1。

function Clock() {
  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      setTime(time + 1);
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, []);

  return (
    <div>Seconds: {time}</div>
  );
}

ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

【问题讨论】:

标签: javascript reactjs react-hooks


【解决方案1】:
function Clock() {
  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      setTime(time => time + 1);// **set callback function here** 
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, []);

  return (
    <div>Seconds: {time}</div>
  );
}

ReactDOM.render(<Clock />, document.querySelector('#app'));

【讨论】:

  • 您的答案可以通过额外的支持信息得到改进。请edit 添加更多详细信息,例如引用或文档,以便其他人可以确认您的答案是正确的。你可以找到更多关于如何写好答案的信息in the help center
【解决方案2】:
  const [loop, setLoop] = useState(0);
  
  useEffect(() => {
    setInterval(() => setLoop(Math.random()), 5000);
  }, []);

  useEffect(() => {
    // DO SOMETHING...
  }, [loop])

【讨论】:

  • 欢迎来到 StackOverflow。虽然您的答案可能会解决问题,但它缺少对您发布的代码的解释。请查看answering questions 上的博客了解更多信息。
【解决方案3】:

正如其他人指出的那样,问题是useState 只被调用一次(如deps = [])来设置间隔:

React.useEffect(() => {
    const timer = window.setInterval(() => {
        setTime(time + 1);
    }, 1000);

    return () => window.clearInterval(timer);
}, []);

然后,每次setInterval 滴答声,它实际上都会调用setTime(time + 1),但time 将始终保持它最初在定义setInterval 回调(闭包)时的值。

您可以使用useState 的设置器的替代形式并提供回调而不是您要设置的实际值(就像setState 一样):

setTime(prevTime => prevTime + 1);

但我鼓励您创建自己的 useInterval 挂钩,以便您可以使用 setInterval declaratively 干燥和简化代码,正如 Dan Abramov 在 Making setInterval Declarative with React Hooks 中建议的那样:

function useInterval(callback, delay) {
  const intervalRef = React.useRef();
  const callbackRef = React.useRef(callback);

  // Remember the latest callback:
  //
  // Without this, if you change the callback, when setInterval ticks again, it
  // will still call your old callback.
  //
  // If you add `callback` to useEffect's deps, it will work fine but the
  // interval will be reset.

  React.useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  // Set up the interval:

  React.useEffect(() => {
    if (typeof delay === 'number') {
      intervalRef.current = window.setInterval(() => callbackRef.current(), delay);

      // Clear interval if the components is unmounted or the delay changes:
      return () => window.clearInterval(intervalRef.current);
    }
  }, [delay]);
  
  // Returns a ref to the interval ID in case you want to clear it manually:
  return intervalRef;
}


const Clock = () => {
  const [time, setTime] = React.useState(0);
  const [isPaused, setPaused] = React.useState(false);
        
  const intervalRef = useInterval(() => {
    if (time < 10) {
      setTime(time + 1);
    } else {
      window.clearInterval(intervalRef.current);
    }
  }, isPaused ? null : 1000);

  return (<React.Fragment>
    <button onClick={ () => setPaused(prevIsPaused => !prevIsPaused) } disabled={ time === 10 }>
        { isPaused ? 'RESUME ⏳' : 'PAUSE ?' }
    </button>

    <p>{ time.toString().padStart(2, '0') }/10 sec.</p>
    <p>setInterval { time === 10 ? 'stopped.' : 'running...' }</p>
  </React.Fragment>);
}

ReactDOM.render(<Clock />, document.querySelector('#app'));
body,
button {
  font-family: monospace;
}

body, p {
  margin: 0;
}

p + p {
  margin-top: 8px;
}

#app {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 100vh;
}

button {
  margin: 32px 0;
  padding: 8px;
  border: 2px solid black;
  background: transparent;
  cursor: pointer;
  border-radius: 2px;
}
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

除了生成更简单、更简洁的代码外,这还允许您通过简单地传递 delay = null 来自动暂停(和清除)间隔并返回间隔 ID,以防您想自己手动取消它(这不在丹的帖子)。

实际上,这也可以改进,使其在未暂停时不会重新启动delay,但我想对于大多数用例来说这已经足够了。

如果您正在寻找setTimeout 而不是setInterval 的类似答案,请查看:https://stackoverflow.com/a/59274757/3723993

您还可以在https://www.npmjs.com/package/@swyg/corre 中找到setTimeoutsetIntervaluseTimeoutuseInterval 的声明式版本,以及一些用TypeScript 编写的附加钩子。

【讨论】:

    【解决方案4】:

    我从这个博客复制了代码。所有功劳归所有者。 https://overreacted.io/making-setinterval-declarative-with-react-hooks/

    唯一的事情是我将此 React 代码改编为 React Native 代码,因此如果您是 React Native 编码员,只需复制此代码并将其调整为您想要的。很容易适应!

    import React, {useState, useEffect, useRef} from "react";
    import {Text} from 'react-native';
    
    function Counter() {
    
        function useInterval(callback, delay) {
            const savedCallback = useRef();
          
            // Remember the latest function.
            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]);
          }
    
        const [count, setCount] = useState(0);
    
      useInterval(() => {
        // Your custom logic here
        setCount(count + 1);
      }, 1000);
      return <Text>{count}</Text>;
    }
    
    export default Counter;
    

    【讨论】:

      【解决方案5】:

      useRef 可以解决这个问题,这里有一个类似的组件,每 1000ms 增加一次计数器

      import { useState, useEffect, useRef } from "react";
      
      export default function App() {
        const initalState = 0;
        const [count, setCount] = useState(initalState);
        const counterRef = useRef(initalState);
      
        useEffect(() => {
          counterRef.current = count;
        })
      
        useEffect(() => {
          setInterval(() => {
            setCount(counterRef.current + 1);
          }, 1000);
        }, []);
      
        return (
          <div className="App">
            <h1>The current count is:</h1>
            <h2>{count}</h2>
          </div>
        );
      }
      

      我认为this article 将帮助您使用间隔进行反应钩子

      【讨论】:

        【解决方案6】:

        按照下面的操作就可以了。

        const [count , setCount] = useState(0);
        
        async function increment(count,value) {
            await setCount(count => count + 1);
          }
        
        //call increment function
        increment(count);
        

        【讨论】:

        • 您的答案中使用 setInterval 的位置?
        • increment 的参数在这里也没用。
        【解决方案7】:

        这个解决方案对我不起作用,因为我需要获取变量并做一些事情而不仅仅是更新它。

        我有一个变通方法,可以通过承诺获取钩子的更新值

        例如:

        async function getCurrentHookValue(setHookFunction) {
          return new Promise((resolve) => {
            setHookFunction(prev => {
              resolve(prev)
              return prev;
            })
          })
        }
        

        有了这个,我可以像这样获取 setInterval 函数中的值

        let dateFrom = await getCurrentHackValue(setSelectedDateFrom);
        

        【讨论】:

        • 这是一种不好的做法,React 状态设置器应该是纯粹的,没有副作用。此外,调用一些 setter 来获取当前值仍然会触发当前组件的重新渲染。
        【解决方案8】:

        useEffect 函数在提供空输入列表时仅在组件安装时评估一次。

        setInterval 的替代方法是在每次状态更新时使用setTimeout 设置新间隔:

          const [time, setTime] = React.useState(0);
          React.useEffect(() => {
            const timer = setTimeout(() => {
              setTime(time + 1);
            }, 1000);
            return () => {
              clearTimeout(timer);
            };
          }, [time]);
        

        setTimeout 的性能影响不大,一般可以忽略。除非组件对时间敏感到新设置的超时会导致不良影响,否则setIntervalsetTimeout 方法都是可以接受的。

        【讨论】:

          【解决方案9】:

          当时间改变时告诉 React 重新渲染。opt out

          function Clock() {
            const [time, setTime] = React.useState(0);
            React.useEffect(() => {
              const timer = window.setInterval(() => {
                setTime(time + 1);
              }, 1000);
              return () => {
                window.clearInterval(timer);
              };
            }, [time]);
          
            return (
              <div>Seconds: {time}</div>
            );
          }
          
          ReactDOM.render(<Clock />, document.querySelector('#app'));
          <script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
          <script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>
          
          <div id="app"></div>

          【讨论】:

          • 这样的问题是每次count更改后定时器都会被清空并重置。
          • 因为正如 Estus 所指出的那样,setTimeout() 是首选
          【解决方案10】:

          原因是因为传入setInterval的闭包的回调仅在第一次渲染中访问time变量,它无法在后续渲染中访问新的time值,因为@987654326 @ 没有被第二次调用。

          timesetInterval 回调中的值始终为 0。

          和你熟悉的setState一样,状态钩子有两种形式:一种是接受更新的状态,另一种是传入当前状态的回调形式。你应该使用第二种形式并阅读最新的setState 回调中的 state 值,以确保在递增之前拥有最新的 state 值。

          奖励:替代方法

          Dan Abramov 在他的blog post 中深入探讨了有关使用带有钩子的setInterval 的主题,并提供了解决此问题的替代方法。强烈推荐阅读!

          function Clock() {
            const [time, setTime] = React.useState(0);
            React.useEffect(() => {
              const timer = window.setInterval(() => {
                setTime(prevTime => prevTime + 1); // <-- Change this line!
              }, 1000);
              return () => {
                window.clearInterval(timer);
              };
            }, []);
          
            return (
              <div>Seconds: {time}</div>
            );
          }
          
          ReactDOM.render(<Clock />, document.querySelector('#app'));
          <script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
          <script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>
          
          <div id="app"></div>

          【讨论】:

          • @YangshunTay 如果我只想读取 setInterval 中的状态值,我该怎么做?
          • @neosarchizo 你读过 Dan 的帖子了吗? overreacted.io/making-setinterval-declarative-with-react-hooks。如果您只想阅读它,您可以在底部的渲染中读取更新后的值。如果你想触发副作用,你可以添加一个useEffect() 钩子并将该状态添加到依赖数组中。
          • 如果您想在 setInterval 函数中使用 console.log 定期输出当前状态会是什么样子?
          • 我想读取时间(在 setInterval 中)并在超过某个时间时更新。如何做到这一点?
          • @neosarchizo "如果你只是想阅读它,你可以阅读更新的值作为底部渲染的一部分。"没看懂,能详细说明一下吗
          【解决方案11】:

          另一种解决方案是使用useReducer,因为它将始终传递当前状态。

          function Clock() {
            const [time, dispatch] = React.useReducer((state = 0, action) => {
              if (action.type === 'add') return state + 1
              return state
            });
            React.useEffect(() => {
              const timer = window.setInterval(() => {
                dispatch({ type: 'add' });
              }, 1000);
              return () => {
                window.clearInterval(timer);
              };
            }, []);
          
            return (
              <div>Seconds: {time}</div>
            );
          }
          
          ReactDOM.render(<Clock />, document.querySelector('#app'));
          <script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
          <script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>
          
          <div id="app"></div>

          【讨论】:

          • 为什么这里的useEffect被多次调用来更新时间,而dependencies数组是空的,这意味着useEffect应该只在组件/应用程序第一次渲染时被调用?
          • @BlackMath useEffect 中的函数仅在组件第一次真正渲染时被调用一次。但是在里面,有一个setInterval负责定时修改时间。我建议你阅读一下setInterval,之后事情应该会更清楚! developer.mozilla.org/en-US/docs/Web/API/…
          猜你喜欢
          • 2021-03-22
          • 2022-12-06
          • 2021-05-30
          • 2019-09-23
          相关资源
          最近更新 更多