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

问题描述 投票:0回答:5

我正在尝试使用 React hook 在屏幕上渲染倒计时器,但我不确定渲染它的最佳方法是什么。

我知道我应该使用

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;```
javascript reactjs react-hooks settimeout
5个回答
83
投票
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>
  );
};

15
投票

您应该使用

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>{timeLeft}s</div>;
}

工作示例:

Countdown example

请注意,在设置器中,我们需要使用此语法

(t) => t - 1
,以便每次都能获得最新值(请参阅:https://reactjs.org/docs/hooks-reference.html#function-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>{timeLeft}s</div>;
}

Countdown example


5
投票

这是 setTimeout 的另一种选择

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

示例

Edit fragrant-currying-512ky


2
投票

这是一个小组件 -

CountdownTimer
- 接受输入参数
expiresIn
表示剩余时间(以秒为单位)。

我们使用 useState 来定义在屏幕上显示的

min
sec
,并且我们还使用
timeLeft
来跟踪剩余时间。

我们使用 useEffect 来递减

timeLeft
并每秒重新计算
min
sec

此外,我们使用 formatTime 来格式化分钟和秒,然后再将其显示在屏幕上。如果分钟和秒都等于 0,我们将停止倒计时器。

import { useState, useEffect } from 'react';


const CountdownTimer = ({expiresIn}) => {
    const [min, setMin] = useState(0);
    const [sec, setSec] = useState(0);
    const [timeLeft, setTimeLeft] = useState(expiresIn);

    const formatTime = (t) => t < 10 ? '0' + t : t;

    useEffect(() => {
        const interval = setInterval(() => {
            const m = Math.floor(timeLeft / 60);
            const s = timeLeft - m * 60;

            setMin(m);
            setSec(s);
            if (m <= 0 && s <= 0) return () => clearInterval(interval);

            setTimeLeft((t) => t - 1);
          }, 1000);

          return () => clearInterval(interval);
    }, [timeLeft]);

    return (
        <>
            <span>{formatTime(min)}</span> : <span>{formatTime(sec)}</span>
        </>
    );
}

export default CountdownTimer;

我们可以选择传递一个setter

setIsTerminated
,以便在倒计时完成后触发父组件中的事件。

const CountdownTimer = ({expiresIn, setIsTerminated = null}) => {
    ...

例如,我们可以在分和秒都等于0时触发:

if (m <= 0 && s <= 0) {
    if (setTerminated) setIsTerminated(true);
    return () => clearInterval(interval);
}

1
投票

这是我的钩子版本,带有“停止”倒计时。 另外,我添加了“fps”(帧数/秒),以显示带小数位的倒计时!

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

interface ITimer {
    timer: number
    startTimer: (time: number) => void
    stopTimer: () => void
}

interface IProps {
    start?: number
    fps?: number
}

const useCountDown = ({ start, fps }: IProps): ITimer => {
    const [timer, setTimer] = useState(start || 0)
    const intervalRef = useRef<NodeJS.Timer>()

    const stopTimer = () => {
        if (intervalRef.current) clearInterval(intervalRef.current)
    }

    const startTimer = (time: number) => {
        setTimer(time)
    }

    useEffect(() => {
        if (timer <= 0) return stopTimer()
        intervalRef.current = setInterval(() => {
            setTimer((t) => t - 1 / (fps || 1))
        }, 1000 / (fps || 1))
        return () => {
            if (intervalRef.current) clearInterval(intervalRef.current)
        }
    }, [timer])

    return { timer, startTimer, stopTimer }
}

export default useCountDown
© www.soinside.com 2019 - 2024. All rights reserved.