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.
Related
I am trying to build a timer with react
for some sort of reason, it is not incrementing properly - the use effect hook gets triggered too many times and I do know why is that happening
instead of incrementing in 1 second intervals it in increments 3 or more second intervals
Maybe you explain why the hook gets triggered so many times and what i could do to resolve the issue
const Timer = () => {
const [timeDisplayed, setTimeDisplayed] = useState(0);
const [startTime, setStartTime] = useState(0)
const [timerOn, setTimerOn] = useState(false);
React.useEffect(() => {
let interval = null;
if (timerOn) {
interval = setInterval(() => {
var delta = Date.now() - startTime;
setTimeDisplayed(timeDisplayed + Math.floor(delta / 1000))
}, 1000);
} else if (!timerOn) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerOn, timeDisplayed]);
const start = () => {
setStartTime(Date.now())
setTimerOn(true)
}
const stop = () => {
setTimerOn(false)
}
const reset = () => {
setTimerOn(false)
setTimeDisplayed(0)
}
return (
<div >
<h2>Timer</h2>
<p>{timeDisplayed}</p>
<div id="buttons">
<button disabled={timerOn} onClick={() => {start()}}>{timeDisplayed===0 ? 'Start' : 'Resume'}</button>
<button disabled={!timerOn} onClick={() => stop()}>Stop</button>
<button disabled={timeDisplayed === 0} onClick={() => reset()}>Reset</button>
</div>
</div>
);
};
export default Timer;```
When you set timerOn you start updating timeDisplayed every second, and because your effect specifies timeDisplayed as a dependency it runs again. timerOn is still true, so it calls setInterval again, and now you have it updating twice every second. Then three times. Four. And so on.
You could fix this by either 1) returning a cleanup function that clears the interval, or 2) setting interval back to null when the timer is off and adding it to your start condition:
if (timerOn && !interval) {
interval = setInterval(() => {
var delta = Date.now() - startTime;
setTimeDisplayed(prev => prev + Math.floor(delta / 1000))
}, 1000);
} else if (interval) {
clearInterval(interval);
interval = null;
}
}, [timerOn, timeDisplayed]);
You useEffect is updating state that it is dependent on. Meaning it calls itself recursively.
Make use of the callback function of your state setter setTimeDisplayed, and remove timeDisplayed from your dependency array.
React.useEffect(() => {
let interval = null;
if (timerOn) {
interval = setInterval(() => {
var delta = Date.now() - startTime;
setTimeDisplayed(prev => prev + Math.floor(delta / 1000))
}, 1000);
} else if (!timerOn) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerOn]);
Here issue is not with the useEffect hook. It's in the logic you are using to update value of timeDisplayed. You are adding previous value with new updated count.
For example :
when timeDisplayed = 1, Math.floor(delta / 1000) returns 2. That's why on next update timeDisplayed'svalue is set to 3 (timeDisplayed + Math.floor(delta / 1000)) instead of 2.
Try adding a console.log statement as shown below to see all values at each update.
React.useEffect(() => {
let interval = null;
if (timerOn) {
interval = setInterval(() => {
var delta = Date.now() - startTime;
console.log('vals',timeDisplayed, Math.floor(delta / 1000),timeDisplayed + Math.floor(delta / 1000) )
setTimeDisplayed(timeDisplayed + Math.floor(delta / 1000))
}, 1000);
} else if (!timerOn) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerOn, timeDisplayed]);
Update you useEffect to this and your problem should be solved.
React.useEffect(() => {
let interval = null;
if (timerOn) {
interval = setInterval(() => {
var delta = Date.now() - startTime;
setTimeDisplayed(Math.floor(delta / 1000))
}, 1000);
} else if (!timerOn) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerOn, timeDisplayed]);
I tried to create a react timer that counts 20 seconds and then stops at 0 seconds. But the problem is it gets struck randomly in between 14,13 or 12 seconds and they keep repeating the same value again and again. Here is my code.
import React, { useEffect,useState } from 'react';
const Airdrop = () => {
const [timer,setTimer] = useState(0);
const [time,setTime] = useState({});
const [seconds,setSeconds] = useState(20);
const startTimer = () =>{
if(timer === 0 && seconds > 0){
setInterval(countDown,1000);
}
}
const countDown = ()=>{
let secondsValue = seconds - 1;
let timeValue = secondsToTime(secondsValue);
setTime(timeValue);
setSeconds(secondsValue);
if(secondsValue === 0){
clearInterval(timer);
}
}
const secondsToTime = (secs)=>{
let hours,minutes,seconds;
hours = Math.floor(secs/(60*60));
let devisor_for_minutes = secs % (60*60);
minutes = Math.floor(devisor_for_minutes/60);
let devisor_for_seconds = devisor_for_minutes % 60;
seconds = Math.ceil(devisor_for_seconds);
let obj = {
"h": hours,
"m": minutes,
"s": seconds
}
return obj;
}
useEffect(() => {
let timeLeftVar = secondsToTime(seconds);
setTime(timeLeftVar);
}, []);
useEffect(() => {
startTimer();
});
return (
<div style={{color:"black"}}>
{time.m}:{time.s}
</div>
)
}
export default Airdrop
When you are using multiple state and they each depend on each other it is more appropriate to use a reducer because state updates are asynchronous. You might update one state based on a stale value.
However in your case, we don't need a reducer because we can derive all the data we need from a single state.
When you set the state, it is not immediately updated and it might cause issues when the next state depend on the last one. Especially when you use it like this:
const newState = state-1;
setState(newState);
However with functional updates we can directly use the last state to set the next one.
setState((prevState)=> prevState-1);
I took the liberty of creating helper functions to make your component leaner. You can copy them in a file in /helpers and import them directly.
I also replaced the interval with a timeout because it is easy get a situation where we don't clear the interval. For example the component is unmounted and without finishing the timer. We then get an interval running indefinitely.
import React, { useEffect, useState } from 'react';
const getHours = (duration) => {
const hours = Math.floor(duration / 3600);
if (hours < 10) return '0' + hours.toString();
return hours.toString();
};
const getMinutes = (duration) => {
const minutes = Math.floor((duration - +getHours(duration) * 3600) / 60);
if (minutes < 10) return '0' + minutes.toString();
return minutes.toString();
};
const getSeconds = (duration) => {
const seconds =
duration - +getHours(duration) * 3600 - +getMinutes(duration) * 60;
if (seconds < 10) return '0' + seconds.toString();
return seconds.toString();
};
const Airdrop = (props) => {
const { duration = 20 } = props;
const [seconds, setSeconds] = useState(duration);
useEffect(() => {
if (seconds === 0) return;
const timeOut = setTimeout(() => {
setSeconds((prevSeconds) => {
if (prevSeconds === 0) return 0;
return prevSeconds - 1;
});
}, 1000);
return () => {
clearTimeout(timeOut);
};
}, [seconds]);
return (
<div style={{ color: 'black' }}>
{getHours(seconds)}:{getMinutes(seconds)}:{getSeconds(seconds)}
</div>
);
};
export default Airdrop;
You are creating a new interval each time the component is rendered because one of the useEffect hooks is missing a dependency array.
const startTimer = () => {
if (timer === 0 && seconds > 0) {
setInterval(countDown,1000);
}
}
useEffect(() => {
startTimer();
});
I suggest the following:
Capture the timer id in a React ref, use a mounting useEffect hook to return a cleanup function to clear any running intervals if/when component unmounts
Remove the time state, it is derived state from the seconds state, just compute it each render cycle
Add a dependency to the useEffect hook starting the timer
Move secondsToTime utility function outside component
Code
const secondsToTime = (secs) => {
let hours, minutes, seconds;
hours = Math.floor(secs / (60 * 60));
let devisor_for_minutes = secs % (60 * 60);
minutes = Math.floor(devisor_for_minutes / 60);
let devisor_for_seconds = devisor_for_minutes % 60;
seconds = Math.ceil(devisor_for_seconds);
return {
h: hours,
m: minutes,
s: seconds
};
};
...
const Airdrop = () => {
const timerRef = useRef(null);
const [seconds, setSeconds] = useState(20);
useEffect(() => {
return () => clearInterval(timerRef.current); // <-- clean up any running interval on unmount
}, []);
const startTimer = () => {
if (timerRef.current === null && seconds > 0) {
timerRef.current = setInterval(countDown, 1000); // <-- capture timer id to stop interval
}
};
const countDown = () => {
setSeconds((seconds) => seconds - 1); // <-- interval callback only decrement time
};
useEffect(() => {
if (seconds === 0) {
clearInterval(timerRef.current);
timerRef.current = null;
setSeconds(20);
}
}, [seconds]); // <-- check seconds remaining to kill interval/reset
useEffect(() => {
startTimer();
}, []); // <-- only start timer once
const time = secondsToTime(seconds); // <-- compute time
return (
<div style={{ color: "black" }}>
{time.m}:{time.s}
</div>
);
};
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'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
This is my attempt to implement a counter.
const [stateTime, setTime] = useState(time);
let countDown = () => {
setTime(stateTime - 1);
};
let intervalTimer = setInterval(countDown, 1000);
setTimeout(() => {
clearInterval(intervalTimer);
}, 5000);
But it doesn't work and I don't know why.
Here is what you can do
const CountDown = ({ seconds }) => {
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>
);
};
In your parent component
<CountDown seconds={60} />