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

3

我正在尝试自己实现倒计时器,只是为了更好地了解Hook。我知道有一些库可以使用,但不想使用它们。我的代码中的问题是,在“timer”函数中无法获取在启动计时器函数中更新的更新状态。我正在尝试实现具有触发启动、停止、恢复和手动触发功能的计时器,其他组件可以使用倒计时组件。

import React, { useState } from 'react';

const Countdown = ({ countDownTimerOpt }) => {
  const [getObj, setObj] = useState({
    formatTimer: null,
    countDownTimer: 0,
    intervalObj: null,
  });

  const { formatTimer, countDownTimer, intervalObj } = getObj;

  if (countDownTimerOpt > 0 && intervalObj === null) {
    startTimer();
  }

  function startTimer() {
    const x = setInterval(() => {
      timer();
    }, 1000);

    setObj((prev) => ({
      ...prev,
      countDownTimer: countDownTimerOpt,
      intervalObj: x,
    }));
  }

  function timer() {
    var days = Math.floor(countDownTimer / 24 / 60 / 60);
    var hoursLeft = Math.floor(countDownTimer - days * 86400);
    var hours = Math.floor(hoursLeft / 3600);
    var minutesLeft = Math.floor(hoursLeft - hours * 3600);
    var minutes = Math.floor(minutesLeft / 60);
    var remainingSeconds = countDownTimer % 60;

    const formatTimer1 =
      pad(days) +
      ':' +
      pad(hours) +
      ':' +
      pad(minutes) +
      ':' +
      pad(remainingSeconds);

    if (countDownTimer === 0) {
      clearInterval(intervalObj);
    } else {
      setObj((prev) => ({
        ...prev,
        formatTimer: formatTimer1,
        countDownTimer: prev['countDownTimer'] - 1,
      }));
    }
  }
  function pad(n) {
    return n < 10 ? '0' + n : n;
  }

  return <div>{formatTimer ? formatTimer : Math.random()}</div>;
};
export default Countdown;

import React, { useState, useEffect } from 'react';
import Timer from '../../components/countdown-timer/countdown.component';

const Training = () => {
  const [getValue, setValue] = useState(0);

  useEffect(() => {
    const x = setTimeout(() => {
      console.log('setTimeout');
      setValue(10000);
    }, 5000);

    return () => clearInterval(x);
  }, []);

  return <Timer countDownTimerOpt={getValue} />;

我不想在训练页面内使用任何设置间隔,因为倒计时组件还会在考试页面中使用。

2个回答

6
通常我会将您的功能结合到一个自定义钩子中,然后在不同的地方使用它。
const useTimer = (startTime) => {
    const [time, setTime] = useState(startTime)
    const [intervalID, setIntervalID] = useState(null)
    const hasTimerEnded = time <= 0
    const isTimerRunning = intervalID != null

    const update = () => {
        setTime(time => time - 1)
    }
    const startTimer = () => {
        if (!hasTimerEnded && !isTimerRunning) {
            setIntervalID(setInterval(update, 1000))
        }
    }
    const stopTimer = () => {
        clearInterval(intervalID)
        setIntervalID(null)
    }
    // clear interval when the timer ends
    useEffect(() => {
        if (hasTimerEnded) {
            clearInterval(intervalID)
            setIntervalID(null)
        }
    }, [hasTimerEnded])
    // clear interval when component unmounts
    useEffect(() => () => {
        clearInterval(intervalID)
    }, [])
    return {
        time,
        startTimer,
        stopTimer,
    }
}

你当然可以添加重置功能或进行其他更改,但使用可能会像这样:

const Training = () => {
    const { time, startTimer, stopTimer } = useTimer(20)
    return <>
        <div>{time}</div>
        <button onClick={startTimer}>start</button>
        <button onClick={stopTimer}>stop</button>
    </>
}

你能解释一下为什么在timer()函数中状态发生改变,但countDownTimer = 0吗? - Siddhesh Nayak
我认为这是因为一个叫做closures的概念。setInterval在第一次渲染时被调用,而timer是在组件内部创建的。因此,timer最初就可以访问countDownTimer。在下一次渲染中,状态会发生改变,并且将创建一个新的timer函数来访问已更改的状态,但是间隔仍将调用旧的timer函数。 - ian
好的,谢谢。根据我的要求,我已经更新了你的代码。在你的自定义钩子中,我返回了setTime,然后在training组件中,我删除了onclick的startTimer,并添加了useEffect。在training组件中进行网络调用,然后使用setTime,最后调用startTimer()。这里setTime正常工作,但是startTimer()没有。 - Siddhesh Nayak

3
您可以按照以下步骤创建一个 useCountDown Hook(使用TypeScript):

代码片段

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

export const useCountDown: (
  total: number,
  ms?: number,
) => [number, () => void, () => void, () => void] = (
  total: number,
  ms: number = 1000,
) => {
  const [counter, setCountDown] = useState(total);
  const [startCountDown, setStartCountDown] = useState(false);
  // Store the created interval
  const intervalId = useRef<number>();
  const start: () => void = () => setStartCountDown(true);
  const pause: () => void = () => setStartCountDown(false);
  const reset: () => void = () => {
    clearInterval(intervalId.current);
    setStartCountDown(false);
    setCountDown(total);
  };

  useEffect(() => {
    intervalId.current = setInterval(() => {
      startCountDown && counter > 0 && setCountDown(counter => counter - 1);
    }, ms);
    // Clear interval when count to zero
    if (counter === 0) clearInterval(intervalId.current);
    // Clear interval when unmount
    return () => clearInterval(intervalId.current);
  }, [startCountDown, counter, ms]);

  return [counter, start, pause, reset];
};

使用演示:https://codesandbox.io/s/usecountdown-hook-56lqv


该链接是一个使用倒计时钩子的代码示例。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接