I am trying to make a countdown timer using React hooks. The seconds part of the timer is working as expected, but I am encountering an issue when updating the minute part. In the below example, I want the timer to start from 05:00 and then on click of a button update to 04:59, 04:58 and so on. But when I click the button, instead of giving 04:59, it gives me 03:59. Attaching the code for the same below. Please let me know where I am getting it wrong.
import React, { useState } from "react";
const padWithZero = num => {
const numStr = num.toString();
return numStr.length === 1 ? "0" + numStr : numStr;
};
const Clock = () => {
let timer;
const [mins, setMins] = useState(5);
const [secs, setSecs] = useState(0);
const startHandler = () => {
timer = setInterval(() => {
setSecs(prevSecs => {
if (prevSecs === 0) {
setMins(prevMins => prevMins - 1);
return 59;
} else return prevSecs - 1;
});
}, 1000);
};
return (
<div>
<h1>{`${padWithZero(mins)}:${padWithZero(secs)}`}</h1>
<button onClick={startHandler}>Start</button>
</div>
);
};
export default Clock;
I don't know the exact reason why it decreases your minutes two times from 5 to 4 and from 4 to 3.
But I modified your code and added resetting feature - each click to button will resets timer back to initial 5:00. And now it works correctly:
import React, { useState } from "react";
const padWithZero = num => {
const numStr = num.toString();
return numStr.length === 1 ? "0" + numStr : numStr;
};
const INIT_SECS = 0;
const INIT_MINS = 5;
const Clock = () => {
let timer;
const [mins, setMins] = useState(INIT_MINS);
const [secs, setSecs] = useState(INIT_SECS);
const [storedTimer, setStoredTimer] = useState(null);
const startHandler = () => {
if (storedTimer) {
clearInterval(storedTimer);
setMins(INIT_MINS);
setSecs(INIT_SECS);
}
const newTimer = setInterval(() => {
setSecs(prevSecs => {
if (prevSecs === 0) {
setMins(prevMins => prevMins - 1);
return 59;
} else return prevSecs - 1;
});
}, 1000);
setStoredTimer(newTimer);
};
return (
<div>
<h1>{`${padWithZero(mins)}:${padWithZero(secs)}`}</h1>
<button onClick={startHandler}>Start</button>
</div>
);
};
export default Clock;
Related
What I am trying to do is to update the reset the countdown after changing the status.
There are three status that i am fetching from API .. future, live and expired
If API is returning future with a timestamp, this timestamp is the start_time of the auction, but if the status is live then the timestamp is the end_time of the auction.
So in the following code I am calling api in useEffect to fetch initial data pass to the Countdown and it works, but on 1st complete in handleRenderer i am checking its status and updating the auctionStatus while useEffect is checking the updates to recall API for new timestamp .. so far its working and 2nd timestamp showed up but it is stopped ... means not counting down time for 2nd time.
import React, { useEffect } from 'react';
import { atom, useAtom } from 'jotai';
import { startTimeAtom, auctionStatusAtom } from '../../atoms';
import { toLocalDateTime } from '../../utility';
import Countdown from 'react-countdown';
import { getCurrentAuctionStatus } from '../../services/api';
async function getAuctionStatus() {
let response = await getCurrentAuctionStatus(WpaReactUi.auction_id);
return await response.payload();
}
const Counter = () => {
// component states
const [startTime, setStartTime] = useAtom(startTimeAtom);
const [auctionStatus, setAuctionStatus] = useAtom(auctionStatusAtom);
useEffect(() => {
getAuctionStatus().then((response) => {
setAuctionStatus(response.status);
setStartTime(toLocalDateTime(response.end_time, WpaReactUi.time_zone));
});
}, [auctionStatus]);
//
const handleRenderer = ({ completed, formatted }) => {
if (completed) {
console.log("auction status now is:", auctionStatus);
setTimeout(() => {
if (auctionStatus === 'future') {
getAuctionStatus().then((response) => {
setAuctionStatus(response.status);
});
}
}, 2000)
}
return Object.keys(formatted).map((key) => {
return (
<div key={`${key}`} className={`countDown bordered ${key}-box`}>
<span className={`num item ${key}`}>{formatted[key]}</span>
<span>{key}</span>
</div>
);
});
};
console.log('starttime now:', startTime);
return (
startTime && (
<div className="bidAuctionCounterContainer">
<div className="bidAuctionCounterInner">
<Countdown
key={auctionStatus}
autoStart={true}
id="bidAuctioncounter"
date={startTime}
intervalDelay={0}
precision={3}
renderer={handleRenderer}
/>
</div>
</div>
)
);
};
export default Counter;
You use auctionStatus as a dependency for useEffect.
And when response.status is the same, the auctionStatus doesn't change, so your useEffect won't be called again.
For answering your comment on how to resolve the issue..
I am not sure of your logic but I'll explain by this simple example.
export function App() {
// set state to 'live' by default
const [auctionStatus, setAuctionStatus] = React.useState("live")
React.useEffect(() => {
console.log('hello')
changeState()
}, [auctionStatus])
function changeState() {
// This line won't result in calling your useEffect
// setAuctionStatus("live") // 'hello' will be printed one time only.
// You need to use a state value that won't be similar to the previous one.
setAuctionStatus("inactive") // useEffect will be called and 'hello' will be printed twice.
}
}
You can simply use a flag instead that will keep on changing from true to false like this:
const [flag, setFlag] = React.useState(true)
useEffect(() => {
// ..
}, [flag])
// And in handleRenderer
getAuctionStatus().then((response) => {
setFlag(!flag);
});
Have a look at the following useCountdown hook:
https://codepen.io/AdamMorsi/pen/eYMpxOQ
const DEFAULT_TIME_IN_SECONDS = 60;
const useCountdown = ({ initialCounter, callback }) => {
const _initialCounter = initialCounter ?? DEFAULT_TIME_IN_SECONDS,
[resume, setResume] = useState(0),
[counter, setCounter] = useState(_initialCounter),
initial = useRef(_initialCounter),
intervalRef = useRef(null),
[isPause, setIsPause] = useState(false),
isStopBtnDisabled = counter === 0,
isPauseBtnDisabled = isPause || counter === 0,
isResumeBtnDisabled = !isPause;
const stopCounter = useCallback(() => {
clearInterval(intervalRef.current);
setCounter(0);
setIsPause(false);
}, []);
const startCounter = useCallback(
(seconds = initial.current) => {
intervalRef.current = setInterval(() => {
const newCounter = seconds--;
if (newCounter >= 0) {
setCounter(newCounter);
callback && callback(newCounter);
} else {
stopCounter();
}
}, 1000);
},
[stopCounter]
);
const pauseCounter = () => {
setResume(counter);
setIsPause(true);
clearInterval(intervalRef.current);
};
const resumeCounter = () => {
setResume(0);
setIsPause(false);
};
const resetCounter = useCallback(() => {
if (intervalRef.current) {
stopCounter();
}
setCounter(initial.current);
startCounter(initial.current - 1);
}, [startCounter, stopCounter]);
useEffect(() => {
resetCounter();
}, [resetCounter]);
useEffect(() => {
return () => {
stopCounter();
};
}, [stopCounter]);
return [
counter,
resetCounter,
stopCounter,
pauseCounter,
resumeCounter,
isStopBtnDisabled,
isPauseBtnDisabled,
isResumeBtnDisabled,
];
};
The Idea is - I press 'start timer' and a function sets the variable 'sessionSeconds' based on whether its work 25mins or rest 5mins session (true/false). Problem is , when interval restarts, function doesn't update this 'currentSessionSeconds'. Maybe it's something with lifecycles or I should use useEffect somehow..
const TimeTracker = () => {
const [work, setWork] = useState(25);
const [rest, setRest] = useState(5);
const [remainingTime,setRemainingTime]= useState(25);
const [iswork, setIswork] = useState(true);
function startTimer() {
clearInterval(interval);
let sessionSeconds = iswork? work * 60 : rest * 60
interval = setInterval(() => {
sessionSeconds--
if (duration <= 0) {
setIswork((iswork) => !iswork)
updateTime(sessionSeconds)
startTimer();
}
}, 1000);
function updateTime(seconds){
setRemainingTime(seconds)
}
}
return (
<div>
<p>
{remainingTime}
</p>
<button onClick={startTimer}>timer</button>
</div>
);
}
I didnt include other code for converting, etc to not over clutter.
I couldn't get your code working to test it since it is missing a }.
I changed your code to include use effect and everything seems to work fine.
https://codesandbox.io/s/wizardly-saha-0dxde?file=/src/App.js
const TimeTracker = () => {
/* In your code you used use state however you didn't change
the state of these variables so I set them to constants.
You can also pass them through props.
*/
const work = 25;
const rest = 5;
const [remainingTime, setRemainingTime] = useState(work * 60);
const [isWork, setIsWork] = useState(true);
const [isTimerActive, setIsTimerActive] = useState(false);
const startTimerHandler = () => {
isWork ? setRemainingTime(work * 60) : setRemainingTime(rest * 60);
setIsTimerActive(true);
};
useEffect(() => {
if (!isTimerActive) return;
if (remainingTime === 0) {
setIsWork((prevState) => !prevState);
setIsTimerActive(false);
}
const timeOut = setTimeout(() => {
setRemainingTime((prevState) => prevState - 1);
}, 1000);
/* The return function will be called before each useEffect
after the first one and will clear previous timeout
*/
return () => {
clearTimeout(timeOut);
};
}, [remainingTime, isTimerActive]);
return (
<div>
<p>{remainingTime}</p>
<button onClick={startTimerHandler}>timer</button>
</div>
);
};
I am trying to create a Pomodoro timer in ReactJS. I am having trouble having the timer to stop it's countdown.
PomView.js
const PomView = () => {
const [timer, setTimer] = useState(1500) // 25 minutes
const [start, setStart] = useState(false)
var firstStart = useRef(true)
var tick;
useEffect( () => {
if (firstStart.current) {
console.log("first render, don't run useEffect for timer")
firstStart.current = !firstStart.current
return
}
console.log("subsequent renders")
console.log(start)
if (start) {
tick = setInterval(() => {
setTimer(timer => {
timer = timer - 1
console.log(timer)
return timer
}
)
}, 1000)
} else {
console.log("clear interval")
clearInterval(tick);
}
}, [start])
const toggleStart = () => {
setStart(!start)
}
const dispSecondsAsMins = (seconds) => {
// 25:00
console.log("seconds " + seconds)
const mins = Math.floor(seconds / 60)
const seconds_ = seconds % 60
return mins.toString() + ":" + ((seconds_ == 0) ? "00" : seconds_.toString())
}
return (
<div className="pomView">
<ul>
<button className="pomBut">Pomodoro</button>
<button className="pomBut">Short Break</button>
<button className="pomBut">Long Break</button>
</ul>
<h1>{dispSecondsAsMins(timer)}</h1>
<div className="startDiv">
{/* event handler onClick is function not function call */}
<button className="startBut" onClick={toggleStart}>{!start ? "START" : "STOP"}</button>
{start && <AiFillFastForward className="ff" onClick="" />}
</div>
</div>
)
}
export default PomView
Although the clearInterval runs in the else portion of useEffect, the timer continues ticking. I am not sure if it is because of the asynchronous setTimer method in useEffect. I would like to know what the problem is with the code I have written.
You store the timer ref in tick, but each time the component rerenders the tick value from the previous render is lost. You should also store tick as a React ref.
You are also mutating the timer state.
setTimer((timer) => {
timer = timer - 1; // mutation
return timer;
});
Just return the current value minus 1: setTimer((timer) => timer - 1);
Code
const PomView = () => {
const [timer, setTimer] = useState(1500); // 25 minutes
const [start, setStart] = useState(false);
const firstStart = useRef(true);
const tick = useRef(); // <-- React ref
useEffect(() => {
if (firstStart.current) {
firstStart.current = !firstStart.current;
return;
}
if (start) {
tick.current = setInterval(() => { // <-- set tick ref current value
setTimer((timer) => timer - 1);
}, 1000);
} else {
clearInterval(tick.current); // <-- access tick ref current value
}
return () => clearInterval(tick.current); // <-- clear on unmount!
}, [start]);
...
};
useEffect( () => {
const tick= setInterval(fun, 1000);
return ()=>{
clearInterval(tick);
}
}, [])
useEffect has it's own release way.
I'm trying to implement countdown timer on my own just to know hooks more. I know there are libraries out there but don't want to use it. the problem with my code is, I cannot get updated state inside "timer" function which is updated in start timer function I'm trying to implement timer that will have triggers to start, stop, & resume & can be manually trigger. by other component that is using the countdown component
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} />;
don't want to use any set interval inside training page as the countdown component will also be used in exam page
Usually with hooks I would combine your functionality into a custom hook and use it in different places.
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,
}
}
You can of course add a reset function or do other changes but use could look like this:
const Training = () => {
const { time, startTimer, stopTimer } = useTimer(20)
return <>
<div>{time}</div>
<button onClick={startTimer}>start</button>
<button onClick={stopTimer}>stop</button>
</>
}
You can create a useCountDown Hook as follow (In Typescript) :
Gist
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];
};
Usage Demo: https://codesandbox.io/s/usecountdown-hook-56lqv
I rewrote my class component to a functional component but I this I made a mistake somewhere. I can find out what it is. The timer keeps going when the time is up. I think its the useEffect that i use wrong? The class componed worked fine but I need the functional to use redux.
import React, {useEffect, useState} from 'react';
import '../style/App.scss';
function CountdownTimer(){
const io = require('socket.io-client');
const socket = io.connect("http://localhost:3001/", {
reconnection: false
});
useEffect(() => {
if(running){
handleStart()
}
});
let timer = null;
const [time, setTime] = useState(0.1*60);
const [running, setRunning] = useState(true);
const handleStart= () => {
if (running){
timer = setInterval(() => {
if (time === 0){
console.log("no more time");
handleStop()
}else{
const newTime = time - 1;
setTime(
newTime >= 0 ? newTime : handleStop
);
}
}, 1000);
}
}
const handleStop = () => {
if(timer) {
clearInterval(timer);
setRunning(false);
}
}
const format = (time) => {
const date = new Date(time * 1000);
let hh = date.getUTCHours();
let mm = date.getUTCMinutes();
let ss = date.getSeconds();
if (hh < 10) {hh = "0"+hh;}
if (mm < 10) {mm = "0"+mm;}
if (ss < 10) {ss = "0"+ss;}
return '00' !== hh ? hh+":"+mm+":"+ss : mm+":"+ss;
}
return(
<div className="icon-value">
<h1>{format(time)}</h1>
</div>
);
}
export default CountdownTimer;