building a speed-type-game in react
in my app there countdown feature using setinterval function
me want to stop countdown when it reaches 0
my code here
const countdownValue = 15
const [sec , setSec ] = useState(countdownValue)
let timer
function start(){
timer = setInterval( () => {
setSec( sec => sec - 1)
},1000)
}
if(sec <= 0){
clearInterval(timer)
}
me tried clearinterval function to stop countdown but its not working
the component re-renders after the state change. So your timer becomes undefined. What you can do is: move it outside your component or use useRef from React
const countdownValue = 15
const [sec , setSec ] = useState(countdownValue)
const timer = useRef()
function start(){
timer.current = setInterval( () => {
setSec( sec => sec - 1)
},1000)
}
useEffect(() => {
if (sec <= 0) {
clearinterval(timer.current)
}
}, [sec])
Related
In my react component, once the component loads, I am trying to repeat a task every 100ms and after 3 repetitions pause for 1 second. Then repeat this pattern indefinitely.
I want to achieve this output:
1 // pause 100ms
2 // pause 100ms
3 // pause 100ms
// pause 1second
... repeat
I tried something like this
useEffect(() => {
let i = 0
function increment() {
if (i === 3) {
// i = 0
// restart timer?
// return?
}
i++
console.log(i)
}
const incrementTimer = setInterval(increment, 100)
setInterval(() => {
clearInterval(incrementTimer)
}, 1000)
}, [])
You can use a recursive function within useEffect hook:
useEffect(() => {
function start(i = 1) {
const reset = i > 3;
const time = reset ? 1000 : 100;
const timeout = setTimeout(() => {
task(i, time); // -> run your task
start(reset ? 1 : i + 1); recursive call to schedule next task
clearTimeout(timeout); // -> clear
}, time);
}
start();
}, []);
function task(i, time) {
console.log('task is running: ', i, time);
}
Working example: https://stackblitz.com/edit/react-jenzmw?file=src%2FApp.js
You can achieve this via setTimeout.
useEffect(() => {
let i = 0
let incrementTimer
function increment() {
if (i === 3) {
// i = 0
// restart timer?
// return?
}
i++
console.log(i)
incrementTimer = setTimeout(increment, 100)
}
increment()
setInterval(() => {
clearInterval(incrementTimer)
}, 1000)
}, [])
First you need to define a state to determine at which level the component is.
const [levelIndex, setLevelIndex] = useState(0);
Then create an array for execution time at each level (This is For the case that you are looking for, it can be changed depending on what time spans are needed or which pattern a user wants).
const intevalExecutionTime = [100,100,100,1000];
Next create a useEffect hook like this:
useEffect(() => {
const timer = setInterval(() => {
//**write the task that you want to be done.**
if(levelIndex === 3){
setLevelIndex(0);
console.log(`one second paused`);
}
else{
setLevelIndex(levelIndex+1);
console.log('task is done.');
}
clearInterval(timer);
}, intevalExecutionTime[levelIndex])
}, [levelIndex]);
This hook callback will set an interval based on the interval execution time level. Every time the setInterval callback is triggered the older interval object will be removed and it changes the levelIndex and the useEffect callback will be called once again.
I am trying to code a countdown clock and the "mins" is from a brother component.
If I take off setMin from useEffect then it is not changing while the value change in source components, but if I leave it in useEffect it will re-rendering every time if the seconds change and makes it immutable. Anyway, can I fix this problem?
If I need useRef how can I use it with setMin?
export default function Timer(props) {
const { count } = props;
const [isPlay, setIsPlay] = useState(false);
const [isRestore, setIsRestore] = useState(false);
const [second, setSecond] = useState(0);
const [isBreak, setIsBreak] = useState(false);
const [min, setMin] = useState(count)
useEffect(() => {
setMin(count)
let alarm = document.getElementById("beep");
const countdown = () => {
setTimeout(() => {
if (second === 0 && min > 0) {
setMin(min-1);
setSecond(59);
} else if (min >= 0 && second > 0) {
setSecond(second - 1);
} else {
alarm.play();
alarm.addEventListener("ended", () => {
setIsBreak(!isBreak);
});
}
}, 1000);
};
if (isPlay) {
countdown();
} else {
alarm.pause();
}
if (isRestore) {
setIsPlay(false);
alarm.currentTime = 0;
setIsRestore(false);
}
},[isPlay, isRestore, second, isBreak,min,count]);
I think the majority of your logic should be outside of the useEffect hook. Take a look at this answer Countdown timer in React, it accomplishes pretty much the same task as yours and should help you get an idea of the necessary logic
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>
);
};
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