我正在尝试使用 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;```
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>
);
};
您应该使用
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>;
}
工作示例:
请注意,在设置器中,我们需要使用此语法
(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>;
}
这是一个小组件 -
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);
}
这是我的钩子版本,带有“停止”倒计时。 另外,我添加了“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