在 React 中使用 Hooks 实现倒数计时器

Posted

技术标签:

【中文标题】在 React 中使用 Hooks 实现倒数计时器【英文标题】:Implementing a countdown timer in React with Hooks 【发布时间】:2019-11-29 21:39:47 【问题描述】:

我正在尝试使用反应挂钩在屏幕上呈现倒计时计时器,但我不确定呈现它的最佳方式。

我知道我应该使用 useEffect 来比较当前状态和以前的状态,但我认为我做得不对。

非常感谢您的帮助!

我尝试了几种不同的方法,但它们都不起作用,例如每当更新时设置状态,但它最终会像疯了一样闪烁。



const Timer = ( seconds ) => 
    const [timeLeft, setTimeLeft] = useState('');

    const now = Date.now();
    const then = now + seconds * 1000;

    const countDown = setInterval(() => 
        const secondsLeft = Math.round((then - Date.now()) / 1000);
        if(secondsLeft <= 0) 
            clearInterval(countDown);
            console.log('done!');
            return;
        
        displayTimeLeft(secondsLeft);
    , 1000);

    const displayTimeLeft = seconds => 
        let minutesLeft = Math.floor(seconds/60) ;
        let secondsLeft = seconds % 60;
        minutesLeft = minutesLeft.toString().length === 1 ? "0" + minutesLeft : minutesLeft;
        secondsLeft = secondsLeft.toString().length === 1 ? "0" + secondsLeft : secondsLeft;
        return `$minutesLeft:$secondsLeft`;
    

    useEffect(() => 
        setInterval(() => 
            setTimeLeft(displayTimeLeft(seconds));
        , 1000);
    , [seconds])


    return (
        <div><h1>timeLeft</h1></div>
    )


export default Timer;```

【问题讨论】:

钩子和间隔的经典问题。看看overreacted.io/making-setinterval-declarative-with-react-hooks 并注意在拆机时取消间隔 【参考方案1】:
const Timer = ( seconds ) => 
  // initialize timeLeft with the seconds prop
  const [timeLeft, setTimeLeft] = useState(seconds);

  useEffect(() => 
    // exit early when we reach 0
    if (!timeLeft) return;

    // save intervalId to clear the interval when the
    // component re-renders
    const intervalId = setInterval(() => 
      setTimeLeft(timeLeft - 1);
    , 1000);

    // clear interval on re-render to avoid memory leaks
    return () => clearInterval(intervalId);
    // add timeLeft as a dependency to re-rerun the effect
    // when we update it
  , [timeLeft]);

  return (
    <div>
      <h1>timeLeft</h1>
    </div>
  );
;

【讨论】:

在这里使用 setTimer 是否更有意义? @AmirShitrit 你的意思是setTimeout?两者都是有效的选项,但setTimeout 假设触发一次函数,而setInterval 假设每 x 时间触发一次函数。由于useEffect 的性质,我们需要在每次timeLeft 更改时设置和清除计时器,我猜它的行为并不像“真实”setInterval,我可以在setTimeout 中看到你的观点这个案例。 是的。我的意思是设置超时。谢谢! @AmirShitrit 是的,我更喜欢 setTimeout 我最喜欢它的地方是它可以工作。【参考方案2】:

您应该使用setInterval。我只是想对@Asaf 解决方案进行一点改进。 您不必每次更改值时都重新设置间隔。它会删除间隔并每次添加一个新间隔(在这种情况下最好使用setTimeout)。所以你可以删除你的useEffect(即[])的依赖:

function Countdown( seconds ) 
  const [timeLeft, setTimeLeft] = useState(seconds);

  useEffect(() => 
    const intervalId = setInterval(() => 
      setTimeLeft((t) => t - 1);
    , 1000);
    return () => clearInterval(intervalId);
  , []);

  return <div>timeLefts</div>;

工作示例:

注意在setter中,我们需要使用(t) =&gt; t - 1这样的语法,这样我们每次都能得到最新的值(见:https://reactjs.org/docs/hooks-reference.html#functional-updates)。


编辑(2021 年 10 月 22 日)

如果您想使用 setInterval 并将计数器停止在 0,您可以执行以下操作:

function Countdown( seconds ) 
  const [timeLeft, setTimeLeft] = useState(seconds);
  const intervalRef = useRef(); // Add a ref to store the interval id

  useEffect(() => 
    intervalRef.current = setInterval(() => 
      setTimeLeft((t) => t - 1);
    , 1000);
    return () => clearInterval(intervalRef.current);
  , []);

  // Add a listener to `timeLeft`
  useEffect(() => 
    if (timeLeft <= 0) 
      clearInterval(intervalRef.current);
    
  , [timeLeft]);

  return <div>timeLefts</div>;

【讨论】:

区间将无限运行 如果您想在 0 处停止,您可以通过卸载组件来停止计数器:seconds &gt; 0 &amp;&amp; &lt;Countdown seconds=seconds /&gt;。但确实,根据要求,它可能需要一些调整。如果您更新道具中的seconds,它也不会更新间隔值。我只是想提供一个有效的替代setTimeout,使用setInterval(而不是在每次渲染时重置它)。 就是这样,父母对timeLeft一无所知,更新每次重新渲染的间隔非常好,并且当timeLeft达到某个点时,您可以选择做事像 0。你可以在状态更新回调中做到这一点,但在我看来这很难看 ESLint 有一个规则 react-hooks/exhaustive-deps 强制在数组中添加 timeLeft 的依赖项。但是,仍然同意不必每次都清除间隔。我认为在这种情况下我将不得不使用setTimeout【参考方案3】:

这是 setTimeout 的另一种选择

const useCountDown = (start) => 
  const [counter, setCounter] = useState(start);
  useEffect(() => 
    if (counter === 0) 
      return;
    
    setTimeout(() => 
      setCounter(counter - 1);
    , 1000);
  , [counter]);
  return counter;
;

例子

【讨论】:

以上是关于在 React 中使用 Hooks 实现倒数计时器的主要内容,如果未能解决你的问题,请参考以下文章

react hooks

react hooks

React 中的倒数计时器

React-Redux 中的倒数计时器功能

ZF_react hooks useEffect的实现 useRef useImperativeHandle的实现,react整体功能实现完毕

React hooks - 清除超时和间隔的正确方法