I'm trying to set a countdown timer using useRef, so that I prevent re-rendering. I want the countdown timer to start counting down once the start button is clicked, then once it gets to 0, stop.
I'm a beginner, so I'm not sure what I'm doing wrong.
I saw this Start the timer when button is clicked in React.Js and I tried implementing it and adding a useRef, but it's not working. Please help, thanks in advance.
const {useState, useRef, useEffect} = React;
const Timer = () => {
const [secondsLeft, setSecondsLeft] = useState(10);
const [start, setStart] = useState(false);
let intervalRef = useRef();
const decreaseSeconds = () => setSecondsLeft((prev) => prev - 1);
useEffect(() => {
intervalRef.current = setInterval(decreaseSeconds, 1000);
return () => clearInterval(intervalRef.current);
}, []);
cosnt handleClick = () => {
let timer = null;
if (start) { // if start button is clicked...
setStart(true);
intervalRef.current = setInterval(decreaseSeconds, 1000); // start timer again
} else {
clearInterval(intervalRef.current); // stop time if button isn't clicked
}
};
return (
<div>
<div>{secondsLeft}</div>
<button onClick={handleClick}>{start ? "Start" : "Cancel"}</button>
</div>
);
}
ReactDOM.render(
<Timer />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You need to clear interval when secondsLeft value get 0.
Everytime whenever decreaseSeconds function get called check for current value and clear interval on desired condition.
const decreaseSeconds = () => {
setSecondsLeft((prev) => {
if (prev === 0) {
clearInterval(intervalRef);
setStart(false);
return 0;
}
else {
return prev - 1;
}
});
}
The clearInterval you used under useEffect get called when this component get unmout/destroy. As this is a Good Practice to add clear timeout/interval function in useEffect so that if component get's unmount timeout/interval function get clear otherwise it will be running in backgroud and cause errors.
Check useEffect Cleanup function
Adding to what Rohit said in his answer, here's a working example. You could use a ref to store the timer, but it's not needed in this case as you have a closure within the useEffect. Note that the dependency array includes the start state, so this useEffect will run each time start is updated.
const Timer = () => {
const [secondsLeft, setSecondsLeft] = React.useState(10);
const [start, setStart] = React.useState(false);
const decreaseSeconds = () => {
setSecondsLeft((prev) => {
if (prev > 0) {
return --prev
} else {
setStart(false)
return 10
}
})
};
React.useEffect(() => {
let timer
if (start) {
timer = setInterval(decreaseSeconds, 1000)
} else {
clearInterval(timer)
}
return () => clearInterval(timer)
}, [start])
const handleClick = () => setStart((prev) => !prev);
return (
<div>
<div>{secondsLeft}</div>
<button onClick={handleClick}>{start ? 'Cancel' : 'Start'}</button>
</div>
)
}
ReactDOM.createRoot(document.getElementById("root")).render(<Timer />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
<div id="root"></div>
Related
The following code demonstrates a react functional component that has a single state variable named time. It has a button click to start which fires a function named updateTimer. This meant to move the timer from 0 to 1 to 2 to 3 and so on.
function timer() {
const [time, updateTime] = useState(0);
function updateTimer() {
setInterval(() => {
updateTime(time + 1)
},1000)
}
}
return (
<>
<span>{time} seconds</span>
<button onClick={updateTimer}>Click To Start</button>
</>
)
But what happens is that the timer stops after 1. Apparently, the value of time does not get updated. Could someone please explain this?
To update the state depending on the previous state use:
updateTime((prevTime) => prevTime + 1)
Because updateTime updates the value of time in the next render of the component, the time known to the setInterval callback is not updated and remained the initial value 0, and resets to 1 at every interval.
In a useEffect run the setter updateTime based on previous value to correct the counting, also cleanup and recreate the interval as needed when the counting starts or stops.
For a basic example, the following useEffect starts an interval when updating is set true by the button. The return runs when
updating changes or when the component unmounted for cleaning up the interval, before a new one can be recreated.
useEffect(() => {
if (!updating) return;
const count = setInterval(() => {
updateTime((prev) => prev + 1);
}, 1000);
return () => {
clearInterval(count);
};
}, [updating]);
updating is added to the dependencies array so that useEffect listens to the changes of this state, and creates a new interval if it is true.
The setter updateTime does not change and it sets time based on the previous value internally, so updateTime and time do not need to be included in the dependencies array.
Example in full snippet:
const { useState, useEffect, Fragment } = React;
function Timer() {
const [time, updateTime] = useState(0);
const [updating, setUpdating] = useState(false);
useEffect(() => {
if (!updating) return;
const count = setInterval(() => {
updateTime((prev) => prev + 1);
}, 1000);
return () => {
clearInterval(count);
};
}, [updating]);
return (
<Fragment>
<p>{time} seconds</p>
<button onClick={() => setUpdating((prev) => !prev)}>{`Click To ${
updating ? "Stop" : "Start"
}`}</button>
</Fragment>
);
}
const App = () => {
return (
<div className="App">
<Timer />
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#root"));
.App {
font-size: x-large;
}
button {
padding: 9px;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>
I wanna stop the looping when stopState is true.
The stopState is updated when I click the stop button. but inside the startIncrement method, the stopState is always false.
This is my code:
function App() {
const [num, setNum] = useState(0)
const [stop, setStop] = useState(false)
function Delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function startIncrement(){
for(let i=0; i<100; i++){
console.log(stop) // always false even when i click the stop button
if(stop) i = 2000;
setNum(i)
await Delay(1000)
}
}
return (
<main>
<p>stop: {stop ? "true" : "false"}</p>
<p onClick={startIncrement}>{num}</p>
<button onClick={() => setStop(true)}>stop</button>
</main>
);
}
Short answer:
In order to stop the previously excuted function, you will have to invoke a `clean up` function.
Detail on how it works:
In react, each render has it's own props and state, and we can explain this in action:
First, you excuted the function startIncrement(), at the time of excution, and at that perticular render phase, the value of stop is false;
Every time the num value changes, the page renders, and it just keep going... (excute below code, you can see console prints "renders!")
Then you click setStop(true) button, and at this particular render, stop === true.
However, these two steps involves two different renders, and each renders' state and everything else (props, effect...), does not affect each other, therefore, your stop value in the function never changes.
Here's an alternative to achieve the same:
export default function App() {
const [num, setNum] = useState(0)
const [stop, setStop] = useState(null)
console.log("renders!")
useEffect(() => {
const run = setInterval(() => {
if(stop === false && num < 100) setNum(num + 1);
}, 1000);
return () => clearInterval(run) // clean up function
}, [num, stop]);
return (
<main>
<p>stop: {stop ? "true" : "false"}</p>
<p>{num}</p>
<button onClick={() => setStop(false)}>start</button>
<button onClick={() => setStop(true)}>stop</button>
</main>
);
}
The clean up function in useEffect can be seen as a "undo" function.
Sandbox here, you can read more about side effect from this post in Dan Abramov's blog
I have the following problem with the useInterval hook, Im trying to make the counter stop if it hits count 20, but it seems that the way im currently implementing it it seems to cause infinite loop of render. here is my code so far:
import React, { ChangeEvent, useState } from "react";
import { useInterval } from "usehooks-ts";
export default function Component() {
// The counter
const [count, setCount] = useState<number>(0);
// Dynamic delay
const [delay, setDelay] = useState<number>(1000);
// ON/OFF
const [isPlaying, setPlaying] = useState<boolean>(true);
useInterval(
() => {
// Your custom logic here
setCount(count + 1);
},
// Delay in milliseconds or null to stop it
isPlaying ? delay : null
);
if (count === 10) {
setPlaying(false);
}
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setDelay(Number(event.target.value));
};
return (
<>
<h1>{count}</h1>
<button onClick={() => setPlaying(!isPlaying)}>
{isPlaying ? "pause" : "play"}
</button>
<p>
<label htmlFor="delay">Delay: </label>
<input
type="number"
name="delay"
onChange={handleChange}
value={delay}
/>
</p>
</>
);
}
I can't grasp on why this wont work, could someone explain and show me what im doing wrong?
The following block in the code is what's causing the infinite loop, as on every render after the count is 10 and counter is paused, setPlaying(false) will be executed every time and will cause re render everytime, thus cause the infinite loop
if (count === 10) {
setPlaying(false);
}
Instead you can move this block to a use effect as
useEffect(() => {
if (count === 10) {
setPlaying(false);
}
}, [count]);
Please find codesandbox for reference
With a useEffect hook :
useEffect(() => {
if (count === 10) {
setPlaying(false);
}
}, [count])
Every time count changes, useEffect will trigger and check if count has reached 10.
You may also prevent user from changing state above 10 :
<button onClick={() => setPlaying(count === 10 ? false : !isPlaying)}>
each update state or props cause to create a new interval.
If you have to use interval
let myinterval = useInterval(
() => {
// Your custom logic here
setCount(count + 1);
},
// Delay in milliseconds or null to stop it
isPlaying ? delay : null
);
clearInterval(myinterval);
you put this code to useEffect
useEffect(()=>{
if(count <20){
let myinterval = useInterval(
() => {
// Your custom logic here
setCount(count + 1);
},
// Delay in milliseconds or null to stop it
isPlaying ? delay : null
);
clearInterval(myinterval);
}
},[count])
I'm working on the simple timer app in the React. Below is the code of what I have.
import React from "react"
const timer = (props) => {
let time = 25;
let processStatus = props.timerProcessStatus; // it comes as true
if(processStatus === true) {
setTimeout(() => {
console.log(proccessStatus);
countDown();
}, 500)
}
function countDown(){
time=time-1;
console.log(time);
}
return (
<div>
<p id={props.statusId}>{props.timerStatus}</p>
<p id={props.timerStatusId}>{time}:00</p>
</div>
)
}
export default timer;
And here is the snapshot of what I get in the browser
As it shows, the time variable changed to 24 (although I would expect it to decrease to 23 second time), it doesn't re-renders in the paragraph, where the content is {time}:00
Incrementing a local variable won't cause a re-render of your component.
You need to put time in the state of your component. Updating the state will cause a re-render of the component which will then update the value of time in the DOM.
import React, {useState} from "react"
const timer = (props) => {
const [time, setTime] = useState(25); // local state
...
function countDown(){
setTime(prevTime => prevTime + 1); // update the state
}
...
}
Every time countDown function will be called, it will update the state, causing a re-render of the component.
I suggest you to read React - State and Lifecycle to understand why you need time as a state of the component.
use the following code for the timer .
import React,{UseEffect,UseState} from 'react';
const timer=(props)=>{
const [time, setTime] = useState(25);
UseEffect(()=>{
let processStatus = props.timerProcessStatus;
// it comes as true
if(processStatus === true) { setTimeout(() => { console.log(proccessStatus); countDown(); }, 500) }
}, [])
function countDown(){
setTime(time--);
}
// other code
setInterval is the method to be used to run code/function in specific interval..
any async task should be run in useEffect
function App(){
const [time,setTime]=React.useState(25)
let processStatus = true; // it comes as true
function countDown() {
// time = time - 1;
setTime(prevTime=>prevTime-1)
// console.log(time);
}
let timer;
React.useEffect(() => {
if (processStatus === true) {
timer = setInterval(() => {
countDown();
}, 500);
}
return ()=>{
clearInterval(timer)
}
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox-{time}</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
ReactDOM.render(<App/>,document.getElementById("root"))
<script src= "https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script src= "https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone#6.15.0/babel.min.js"></script>
<div id="root"></div>
My guess is that it tries to create the time variable each time
so I would suggest to put the time variable in its parent component then just use
function countDown(){
let newTime = time;
props.setTime(newTime - 1);
console.log(newTime - 1);
}
Im having a lot of trouble with this and i have tried various things. I want to call a function every second after i have clicked a start button and then have it paused after i click a stop button. I keep getting weird behaviour that i cant explain.
How can i do this in react without classes?
somethings i have treid:
const simulation = () => {
if (!running) {
console.log('hit');
return
} else {
// console.log(grid);
console.log('hey');
setTimeout(simulation, 1000)
}
}
and
enter setInterval(() => {
let newGrid = [...grid]
for (let i = 0; i < numRow; i++) {
for (let k = 0; k < numCol; k++) {
let n = 0;
}
}
console.log(grid);
}, 5000)
I have tried a lot more, In some cases it would update the state should i have added to it but not updated it after i reset the state.
How can i call a function to run every one second with updated values of state * Note the function that i want to run will update the state
You may do the following:
keep track of the current counter value along with the counter on/off state in your component state;
employ useEffect() hook to be called upon turning counter on/off or incrementing that;
within useEffect() body you may call the function, incrementing count by one (if ticking is truthy, hence timer is on) with delayed execution (using setTimeout());
once count variable is changed in the state, useEffect() gets called once again in a loop;
in order to clean up the timer upon component dismantle, you should return a callback, clearing the timer from useEffect()
const { useState, useEffect } = React,
{ render } = ReactDOM,
rootNode = document.getElementById('root')
const App = () => {
const [ticking, setTicking] = useState(true),
[count, setCount] = useState(0)
useEffect(() => {
const timer = setTimeout(() => ticking && setCount(count+1), 1e3)
return () => clearTimeout(timer)
}, [count, ticking])
return (
<div>
<div>{count}</div>
<button onClick={() => setTicking(false)}>pause</button>
<button onClick={() => setTicking(true)}>resume</button>
</div>
)
}
render (
<App />,
rootNode
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>
Late reply but maybe interesting for some people.
Look at npm package cron. In this case every 15min As easy as:
const [job] = useState(new cron.CronJob("0 */15 * * * *",async ()=>{
await updateRiderCoords();
}));
useEffect(() => {
job.start();
}, []);