200

I'm trying out the new React Hooks and have a Clock component with a counter which is supposed to increase every second. However, the value does not increase beyond one.

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>

4

13 回答 13

243

原因是传入setInterval's 闭包的回调只访问time第一次渲染中的变量,它无法访问time后续渲染中的新值,因为useEffect()第二次没有调用。

timesetInterval回调中的值始终为 0 。

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

奖励:替代方法

Dan Abramov 在他的博客文章中深入探讨了有关使用setIntervalwith hooks的主题,并提供了解决此问题的替代方法。强烈推荐阅读!

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>

于 2018-10-27T17:25:13.103 回答
38

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

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

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

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

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

setTime(prevTime => prevTime + 1);

但是我鼓励你创建自己的useInterval钩子,这样你就可以通过使用setInterval 声明方式来干燥和简化你的代码,正如 Dan Abramov 在使用 React Hooks 制作 setInterval 声明中所建议的那样:

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,以防您想自己手动取消它(Dan 的帖子中没有涉及)。

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

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

您还可以在https://www.npmjs.com/package/@swyg/corresetTimeout中找到and 和 and 的声明式版本,以及setInterval一些useTimeoutuseIntervalTypeScript 编写的附加钩子。

于 2019-12-10T19:20:05.487 回答
30

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

另一种方法是在每次更新状态时setInterval设置新的时间间隔:setTimeout

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

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

于 2019-01-28T07:55:05.347 回答
12

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

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

我认为这篇文章将帮助您了解如何使用间隔进行反应钩子

于 2021-04-16T21:53:06.410 回答
9

另一种解决方案是使用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>

于 2019-01-28T08:22:29.687 回答
1

执行以下操作可以正常工作。

const [count , setCount] = useState(0);

async function increment(count,value) {
    await setCount(count => count + 1);
  }

//call increment function
increment(count);
于 2020-02-04T06:12:54.950 回答
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'));
于 2021-12-20T06:28:47.790 回答
1

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

我得到了一个解决方法来获得带有承诺的钩子的更新值

例如:

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

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

let dateFrom = await getCurrentHackValue(setSelectedDateFrom);
于 2019-08-27T16:56:15.687 回答
1
const [seconds, setSeconds] = useState(0);
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds((seconds) => {
        if (seconds === 5) {
          setSeconds(0);
          return clearInterval(interval);
        }
        return (seconds += 1);
      });
    }, 1000);
  }, []);

注意:这将有助于使用 useState 挂钩更新和重置计数器。秒将在 5 秒后停止。因为首先更改 setSecond 值,然后在 setInterval 内使用更新的秒数停止计时器。作为 useEffect 运行一次。

于 2022-02-18T08:16:23.110 回答
0
  const [loop, setLoop] = useState(0);
  
  useEffect(() => {
    setInterval(() => setLoop(Math.random()), 5000);
  }, []);

  useEffect(() => {
    // DO SOMETHING...
  }, [loop])
于 2021-12-11T21:47:05.830 回答
0

我从这个博客复制了代码。所有功劳归所有者。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;
于 2021-10-12T03:10:24.073 回答
0

对于那些寻求极简解决方案的人:

  1. N秒后停止间隔,并且
  2. 能够在按钮单击时再次重置它多次。

无论如何我都不是 React 专家,我的同事要求帮忙,我写了这篇文章并认为其他人可能会觉得它有用。


  const [disabled, setDisabled] = useState(true)
  const [inter, setInter] = useState(null)
  const [seconds, setSeconds] = useState(0)

  const startCounting = () => {
    setSeconds(0)
    setDisabled(true)
    setInter(window.setInterval(() => {
        setSeconds(seconds => seconds + 1)
    }, 1000))
  }

  useEffect(() => {
      startCounting()
  }, [])

  useEffect(() => {
    if (seconds >= 3) {
        setDisabled(false)
        clearInterval(inter)
    }
  }, [seconds])

  return (<button style = {{fontSize:'64px'}}
      onClick={startCounting}
      disabled = {disabled}>{seconds}</button>)
}
于 2022-02-07T17:00:42.263 回答
-1

当时间改变时告诉 React 重新渲染。选择退出

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>

于 2019-03-20T10:37:25.320 回答