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>
);
};
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 have uploaded my code here in https://codesandbox.io/s/blissful-lehmann-o4thw?file=/src/Layout.tsx
I am trying to do a stopwatch. But working not as expected due to hook issue.
const startTimer = async () => {
setDisableStart(true);
setDisableStop(false);
const rightNowTime = await Date.now().toString();
setCurrentTime(rightNowTime);
interval = setInterval(() => {
calculateTime();
}, 1);
};
I can see problem with setCurrentTime(rightNowTime) is not updating current time
Please, somebody, suggest
You are making things a bit more complicated than they need to be. I suggest storing only a start time and a current "tick" and compute the derived "state" of the minutes, seconds, and milliseconds between these two timestamps.
const App = () => {
const [startTime, setStartTime] = useState<number>(0);
const [currentTime, setCurrentTime] = useState<number>(0);
// Store interval id in React ref
const intervalRef = useRef<number | undefined>();
const [disableStop, setDisableStop] = useState<boolean>(true);
const [disableReset, setDisableReset] = useState<boolean>(true);
const [disableStart, setDisableStart] = useState<boolean>(false);
// Return cleanup function to clear any running intervals
// on the event of component unmount
useEffect(() => {
return () => clearInterval(intervalRef.current);
}, []);
const calculateTime = () => {
setCurrentTime(Date.now());
};
const startTimer = () => {
setDisableStart(true);
setDisableStop(false);
// Only update start time if reset to 0
if (!startTime) {
setStartTime(Date.now());
}
// Invoke once immediately
calculateTime();
// Instantiate interval
intervalRef.current = setInterval(() => {
calculateTime();
}, 1);
};
const stopTimer = () => { ... };
const resetTimer = () => { ... };
// Compute the minutes, seconds, and milliseconds from time delta
const delta: number = currentTime - startTime; // in ms
const minutes: string = Math.floor(delta / (1000 * 60)).toString();
const seconds: string = (Math.floor(delta / 1000) % 60)
.toString()
.padStart(2, "0");
const milliseconds: string = (delta % 1000).toString().padStart(3, "0");
return (
<React.Fragment>
<div className="container">
<h1>Stop Watch</h1>
</div>
<Layout seconds={seconds} minutes={minutes} milliseconds={milliseconds} />
<Buttons ... />
</React.Fragment>
);
};
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've created a simple timer script initialized with 10 seconds for testing. The problem I'm having is that the pause timer button isn't working as expected.
import React, { useState } from 'react';
function App() {
const [time, updateTime] = useState(() => { return 10 });
const [timerRunning, setTimerRunning] = useState(false);
let minutes = Math.floor(time / 60);
let seconds = time % 60;
let interval;
function startTimer() {
setTimerRunning(true);
interval = setInterval( function() {
updateTime(previousTime => previousTime === 0 ? previousTime : previousTime - 1);
}, 1000);
}
function pauseTimer() {
setTimerRunning(false);
clearInterval(interval);
}
function restartTimer() {
setTimerRunning(false);
updateTime(() => {return 10});
}
return (
<>
<p>{minutes > 9 ? minutes : "0" + minutes}:{seconds > 9 ? seconds : "0" + seconds}</p>
<button onClick={startTimer}>Start</button>
<button onClick={pauseTimer}>Pause</button>
<button onClick={restartTimer}>Restart</button>
</>
)
}
export default App;
I want the pause button to pause the timer. Eventually I'll make conditional statements to have each button appear based on the state of the app and the value of time, but the pause button is my current obstacle.
I first had a separate countdown function which used a conditional to stop the time when the time matched counter (below). I thought of a less complicated way that lets me omit the counter variable (above). Im not sure which option is better, or if either is preventing the clearInterval function to work properly. The clearInterval function works within the countdown function if statement, but will not work outside of it.
import React, { useState } from 'react';
function App() {
const [time, updateTime] = useState(() => { return 10 });
const [timerRunning, setTimerRunning] = useState(false);
let counter = 0;
let minutes = Math.floor(time / 60);
let seconds = time % 60;
let interval;
function countdown() {
counter++;
if ( counter === time ) {
setTimerRunning(false);
clearInterval(interval);
}
updateTime(previousTime => previousTime - 1);
}
function startTimer() {
setTimerRunning(true);
interval = setInterval(countdown, 1000);
}
function pauseTimer() {
setTimerRunning(false);
clearInterval(interval);
}
function restartTimer() {
setTimerRunning(false);
updateTime(() => {return 10});
}
return (
<>
<p>{minutes > 9 ? minutes : "0" + minutes}:{seconds > 9 ? seconds : "0" + seconds}</p>
<button onClick={startTimer}>Start</button>
<button onClick={pauseTimer}>Pause</button>
<button onClick={restartTimer}>Restart</button>
</>
)
}
export default App;
Basically you can't create let interval; and assign it a setInterval like interval = setInterval(countdown, 1000);
because on each re-render there will be new let interval;
what you need to do is create a variable which isn't change on re-redners, you can use useRef
const interval = useRef(null);
.....
function startTimer() {
interval.current = setInterval(countdown, 1000);
}
....
function pauseTimer() {
clearInterval(interval.current);
}
and I don't think you need const [timerRunning, setTimerRunning] = useState(false);
find a demo here
Basically when functional component re-renders it will execute from top to bottom, if you use like let counter = 0;, then on each re-render it will initialize to 0, if you need to persists your values in each re-renders you might need some hooks (Ex: useState, useRef ...), in this case useRef would do the trick (because you need only one setInterval in each re-renders and useRef will give you the previous value, it will not re-initalize like a general variable)
You have to use useEffect, like this:
const handleStart = () => {
setChangeValue(true)
}
const handlePause = () => {
setChangeValue(false)
pauseTimer()
}
const handleRestart = () => {
setInitialState()
setChangeValue(true)
}
useEffect(() => {
if (changeValue) {
const interval = setInterval(() => {
startTimer()
}, 100)
return () => clearInterval(interval)
}
}, [changeValue])
you have three buttons to start, pause and restart, invoke these (handleStart, handlePause, handleRestart) functions with them
that is my solution
instead of the startTime function, I use useEffect
useEffect(()=>{
interval = timerRunning && setInterval(() => {
updateTime(previousTime => previousTime === 0 ? previousTime : previousTime - 1);
}, 1000);
return ()=> clearInterval(interval)
},[timerRunning])
and in onClick Start Button
<button onClick={()=> setTimerRunning(true)}>Start</button>
I hope it is useful
I am beginner.
I have a question, I have made a counter timer in React, but unfortunately, it doesn't work properly.
I can't find a mistake, Could someone help me?
import React, { useState, useEffect } from "react";
export default function CountDown() {
let [seconds, setSeconds] = useState(3);
let [minutes, setMinutes] = useState(59);
let [hours, setHours] = useState(3);
useEffect(() => {
const interval = setInterval(() => {
setCounddownTimer();
console.log("i am working", { seconds, minutes, hours });
return () => clearInterval(interval);
}, 1000);
}, []);
const setCounddownTimer = () => {
if (hours === 0 && minutes === 0 && seconds === 0) {
timerReset();
} else if (minutes === 0 && seconds === 0) {
console.log({ seconds, minutes, hours });
setHours(--hours);
setMinutes(59);
setSeconds(59);
} else if (seconds === 0) {
setSeconds(59);
setMinutes(--minutes);
} else {
setSeconds(--seconds);
}
};
const timerReset = () => {
setSeconds(59);
setMinutes(59);
setHours(3);
};
const addLeadingZero = (number) => {
return number < 10 ? "0" + number : number;
};
const style = {
"text-align": "center",
"font-weight": "bold",
color: "#cf0000"
};
return (
<div style={style}>
{addLeadingZero(hours)}:{addLeadingZero(minutes)}:
{addLeadingZero(seconds)}
</div>
);
}
I am also paste a link to codesandbox https://codesandbox.io/s/epic-hopper-f9dy4?file=/src/App.js:0-1277
Your useEffect's return statement was inside the setInterval(). It should be outside.
const interval = setInterval(() => {
setCounddownTimer();
console.log("i am working", { seconds, minutes, hours });
}, 1000);
return () => clearInterval(interval);
UPDATE
Also, you should've put hours, minutes, seconds in useEffect's dependency array, since rendering depends on these 3 states. Otherwise the seconds will count down till the timer reaches 03:58:59, but then the minutes will start counting down.