Using setinterval with updating a state in a functional component - javascript

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>

Related

I have a button that increment a counter when users click on it, but I want to delay the updating process if users click really fast

I have a button that increments a counter with useState hook when users click on it, but I want to know if there is a way to delay the state updating for 0.5 seconds when users click on the button really fast, then update the counter at once. For example, when users click on the button 1 times each second, the counter will be updated immediately. But if users click more than 3 times in one second, the state will not be updated immediately, and the counter will only be updated when users stop clicking fast. The counter will be updated to the number of clicks during the delay. I tried to use setTimeOut but it did not work. Is there a hook for this?
function App() {
// State to store count value
const [count, setCount] = useState(0);
// Function to increment count by 1
const incrementCount = () => {
// Update state with incremented value
setCount((prev)=>{
return prev+1
});
};
return (
<div className="app">
<button onClick={incrementCount}>Click Here</button>
{count}
</div>
);
}
You need to apply Javascript Throttle function. Debounce is not an ideal solution here because with Debounce even after the first click user will have to wait for some time(delay) before execution happens. What you want is that on the first click counter should be incremented but after that if user clicks too fast it should not happen untill some delay ,that what Throttle function provides.
Also Thing to note that to use Throttle or Debounce in React application you will need an additional hook i.e. useCallback, which will not redfeine the function on every re-render and gives a memoized function.
More on difference between Throttle and Debounce :https://stackoverflow.com/questions/25991367/difference-between-throttling-and-debouncing-a-function#:~:text=Throttle%3A%20the%20original%20function%20will,function%20after%20a%20specified%20period.
Let's look at the code :
import { useState, useCallback } from "react";
function App() {
// State to store count value
const [count, setCount] = useState(0);
// Throttle Function
const throttle = (func, limit = 1000) => {
let inThrottle = null;
return (...args) => {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
};
// Function to increment count by 1
const incrementCount = useCallback(
throttle(() => {
// Update state with incremented value
setCount((prev) => {
return prev + 1;
});
}, 1000),
[]
);
return (
<div className="app">
<button onClick={incrementCount}>Click Here</button>
{count}
</div>
);
}
export default App;
This is pure adhoc implementation. I just tried with two state variable and simple implementation. Basically,
firstly at initial click I'm doing count variable 1 instantly. Then, after each click, it will take 1 second for updating count state.
Then, I put a if block in setTimeout() method which is, if the difference between current count value and previous count value is 1, then the count variable will update. The checking is because, on each click the count variable increasing very fast. So, the condition becomes obstacle for that.
import { useState } from "react";
function App() {
// State to store count value
const [count, setCount] = useState(0);
const [prevCount, setPrevCount] = useState(0);
// Function to increment count by 1
const incrementCount = () => {
setPrevCount(count);
if(count === 0) setCount(1);
setTimeout(() => {
if(count - prevCount === 1) {
setCount(prev => prev + 1);
}
}, 1000);
};
return (
<div className="app">
<button onClick={incrementCount}>Click Here</button>
{count}
</div>
);
}
export default App;

Running two countdown timers sequentially in React. The second one is skipped

I have created a functional component called Timer. I want to run a Timer for 3 seconds followed by a Timer for 6 seconds. However, the second timer stops as soon as it starts.
I think the problem is that the second Timer is using the state value from the first Timer. I'm not sure why it does that. I assume that the second Timer is a new instance so it shouldn't have any link with the first Timer.
Here's my code:
import React, { useEffect, useState } from 'react';
function App() {
const [timer1Up, setTimer1Up] = useState(false);
const [timer2Up, setTimer2Up] = useState(false);
if(!timer1Up) {
return <Timer duration={3} setTimeUp={setTimer1Up}/>
}
if(timer1Up && !timer2Up) {
return <Timer duration={6} setTimeUp={setTimer2Up}/>
}
if(timer1Up && timer2Up) {
return <>Out of all your time buddy!!</>
}
return <>I have no purpose</>;
}
interface TimerProps {
duration: number;
setTimeUp: any;
}
const Timer = (props: TimerProps) => {
const [timeLeft, setTimeLeft] = useState(props.duration);
useEffect(() => {
setTimeout(() => {
setTimeLeft(timeLeft - 1);
}, 1000);
if(timeLeft === 0) {
props.setTimeUp(true);
}
});
return <>{timeLeft} s</>
}
export default App;
if(!timer1Up) {
return <Timer duration={3} setTimeUp={setTimer1Up}/>
}
if(timer1Up && !timer2Up) {
return <Timer duration={6} setTimeUp={setTimer2Up}/>
}
The main way react uses to tell whether it needs to mount a new component or reuse an existing one is by the component's type. The first time this renders, you return a <Timer>, so react mounts a new Timer component, which then starts doing a countdown. Once the first countdown is done you render again and you also return a <Timer>. So as far as react can tell, the only thing that changed was the props on that timer. React keeps the same component mounted, with the same state.
So there are two options 1) force it to remount, or 2) let Timer reset if its props change
To force it to remount, you will need to use keys to make it clear to react that these are different elements. That way it will unmount the old timer and mount a new one, and that new one can have its own state that it counts down
if(!timer1Up) {
return <Timer key="first" duration={3} setTimeUp={setTimer1Up}/>
}
if(timer1Up && !timer2Up) {
return <Timer key="second" duration={6} setTimeUp={setTimer2Up}/>
}
To make it work with changing props, you'll need to add logic to reset the countdown. You'll have to decide what conditions should reset the countdown, but one option is that any time the duration changes, you start over:
const Timer = (props: TimerProps) => {
const [timeLeft, setTimeLeft] = useState(props.duration);
useEffect(() => {
setTimeLeft(props.duration);
const intervalId = setInterval(() => {
setTimeLeft(prev => {
const next = prev -1;
if (next === 0) {
clearInterval(intervalId);
// Need to slightly delay calling props.setTimeUp, because setting
// state in a different component while in the middle of setting
// state here can cause an error
setTimeout(() => props.setTimeUp(true));
}
return next;
});
}, 1000);
return () => { clearInterval(intervalId); }
}, [props.duration]); // <---- dependency array to reset when the duration changes
return <>{timeLeft} s</>
}
For more information on how react decides to mount/unmount components, see this page on reconciliation.

Timer not running inside the React Component

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);
}

How to call a function every x seconds with updated state (React)

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();
}, []);

How to show every array object in react.js after specific time interval?

I have an array with 3 elements. I want to loop through it such that after 3 seconds the next element of array is rendered in place of the previous one and if last element is rendered then it should restart again. It's like after 3 seconds I want to display the next element in place of previous one as like looping through the array but showing one element at a time. I have tried the following code for it.
import React, { useEffect, useState } from 'react';
import './text.css';
const Text = () => {
var work = ["fighting", "learning", "qwerty"];
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
if(seconds===2) setSeconds(seconds => seconds = 0);
else setSeconds(seconds => seconds + 1);
console.log(seconds);
}, 3000);
}, []);
return (
<div>
<h1>{work[seconds]}</h1>
</div>
);
}
export default Text;
It is successfully rendering elements one after another but can't render anything after the last element. Also on console the value of seconds is always showing 0.
The issue is that the initial state value is enclosed in the scope of the callback and never updates the state value in the outer scope, but defining the callback outside allows the state to update. Also, increment an index value instead and take the modulus of the array length to always get a valid index.
import React, { useEffect, useState } from "react";
const work = ["fighting", "learning", "qwerty"];
export default function App() {
const [index, setIndex] = useState(0);
useEffect(() => {
const tick = () => setIndex(i => i + 1);
const id = setInterval(tick, 3000);
return () => clearInterval(id);
}, []);
return (
<div>
<h1>{work[index % work.length]}</h1>
</div>
);
}
You were not getting the updated second value inside the setInterval.
Also, clearInterval in useEffect.
Working demo
Refactored code
useEffect(() => {
const interval = setInterval(() => {
setSeconds(second => (second === 2 ? 0 : second + 1));
}, 3000);
return () => clearInterval(interval)
}, []);
Update:
The console.log inside setInterval's callback will always be 0 because the seconds inside the setInterval's callback is the value taken when the callback was registered with setInterval.
To see up-to-date values of second, use another useEffect with seconds as dependency.
useEffect(() => {
console.log("seconds", seconds);
}, [seconds]);

Categories