I am trying to update (increment) a React state (counter) every setInterval function but it is don't work as expected. For the first 5 or so seconds, the count is rendered with consistent increments but after that it increases randomly and doesn't stop.
export default function App() {
const [count, setCount] = useState(0);
setInterval(() => setCount((oldCount) => oldCount + 1), 1000);
return (<>
<div>{count}</div>
</>);
};
How can I achieve this and the same for time intervals less than a second ( 100 or 10ms ) ?
You need to run the interval in useEffect there are plenty of such examples:
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount((oldCount) => oldCount + 1), 1000);
return () => {
clearInterval(id);
};
}, []);
return (
<>
<div>{count}</div>
</>
);
}
Related
I am a beginner using useEffect in React to update the index state of an array on an interval. I want the index to increase by 1 every five seconds, and when it reaches the end of the array loop back to 0.
I put an 'if' statement to check for this within my useEffect function, but it doesn't seem to be firing. When the array reaches the end, my console shows an error reading from an undefined index.
const [idx, setIdx] = useState(0);
useEffect(() => {
const interval = setInterval(() => setIdx((previousValue) => previousValue
+1), 5000);
if(idx > testimonials.length-1) { setIdx(0)};
return () => {
clearInterval(interval);
};
}, []);
Can anyone see what I'm doing wrong?
Use the remainder operator (%):
const [idx, setIdx] = useState(0);
useEffect(() => {
const interval = setInterval(
() => setIdx(idx => (idx + 1) % testimonials.length),
5000,
);
return () => clearInterval(interval);
}, []);
You need to pass the idx value to the useEffect dependencies array.
import React from 'react';
const testimonials = [1, 2, 3, 4, 5, 6];
export default function App() {
const [idx, setIdx] = React.useState(0);
React.useEffect(() => {
const interval = setInterval(
() => setIdx((previousValue) => previousValue + 1),
1000
);
if (idx > testimonials.length - 1) {
setIdx(0);
}
return () => {
clearInterval(interval);
};
}, [idx]);
return (
<div>
<h1>{idx}</h1>
</div>
);
}
you should use this statement
if(idx > testimonials.length-1) { setIdx(0)};
outside the useEffect and above the return which is returning the JSX.
Why is it that the correct count value can be obtained in setinterval after the first click, and then the transformation does not occur again?
import React, { useEffect, useState } from 'react';
const Demo1 = () => {
let [count, setCount] = useState(1);
const onCountClick = () => {
count += 1;
setCount(count);
};
useEffect(() => {
setInterval(() => {
console.log(count);
}, 1000);
}, []);
console.log(count);
return <button onClick={() => onCountClick()}>test</button>;
};
You are directly modifying the state. Instead do this:
setCount(count++)
React doen't really handle setInterval that smoothly, you have to remember that when you put it in componentDidMount (useEffect with an empty dependencies' array), it builds its callback with the current values, then never updates.
Instead, put it inside componentDidUpdate (useEffect with relevant dependencies), so that it could have a chance to update. It boils down to actually clearing the old interval and building a new one.
const Demo1 = () => {
let [count, setCount] = useState(1);
let [intervalId, setIntervalId] = useState(null);
const onCountClick = () => {
count += 1;
setCount(count);
};
useEffect(() => {
setIntervalId(setInterval(() => {
console.log(count);
}, 1000));
}, []);
useEffect(() => {
clearInterval(intervalId);
setIntervalId(setInterval(() => {
console.log(count);
}, 1000));
}, [count]);
console.log(count);
return <button onClick={() => onCountClick()}>test</button>;
};
The first thing is that changing the value of state directly like count += 1 is a bad approach, instead use setCount(count + 1) and you cannot console.log any value in the return statement instead use {count} to display the value on the screen instead of console.
The following code will increment the value of count on every click instance
const [count, setCount] = useState(1);
const onCountClick = () => {
// count += 1;
setCount(count + 1);
};
useEffect(() => {
setInterval(() => {
console.log(count);
}, 1000);
});
return (
<div className="App">
<button onClick={() => onCountClick()}>test</button>;
</div>
);
I am unable to access state in a React functional component from a setInterval or setTimeout callback.
A very simple example updating state in one interval and attempting to access state with bound function and unbound arrow interval callbacks.
https://codesandbox.io/s/pedantic-sinoussi-ycxin?file=/src/App.js
const IntervalExample = () => {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval1 = setInterval(() => {
setSeconds((seconds) => seconds + 1);
}, 1000);
const interval2 = setInterval(() => {
// this retrieves only the initial state value of 'seconds'
console.log("interval2: seconds elapsed", seconds);
}, 1000);
const interval3 = setInterval(function () {
// this retrieves only the initial state value of 'seconds'
console.log("interval3: seconds elapsed", seconds);
}, 1000);
return () => {
clearInterval(interval1);
clearInterval(interval2);
clearInterval(interval3);
};
}, []);
return (
<div className="App">
<header className="App-header">
{seconds} seconds have elapsed since mounting.
</header>
</div>
);
};
I would like to make this work in both function and arrow cases. Any help appreciated.
Thanks
You are seeing initial state values inside setInterval due to stale closure, you can use setSeconds (state setter function) as a workaround to fix the issue of stale closure:
const IntervalExample = () => {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval1 = setInterval(() => {
setSeconds((seconds) => seconds + 1);
}, 1000);
const interval2 = setInterval(() => {
let s = 0;
setSeconds((seconds) => {s = seconds; return seconds;}); // HERE
console.log("interval2: seconds elapsed", s);
}, 1000);
const interval3 = setInterval(function () {
let s = 0;
setSeconds((seconds) => {s = seconds; return seconds;}); // HERE
console.log("interval3: seconds elapsed", s);
}, 1000);
return () => {
clearInterval(interval1);
clearInterval(interval2);
clearInterval(interval3);
};
}, []);
return (
<div className="App">
<header className="App-header">
{seconds} seconds have elapsed since mounting.
</header>
</div>
);
};
You can also use ref variables if you don't need to show seconds at UI. Note that returning the same value i.e. seconds in setSeconds function would not cause a re-render.
Ive used the useStateAndRef hook which works for both function and arrow callbacks.
import "./styles.css";
import React, { useState, useRef, useEffect } from "react";
function useStateAndRef(initial) {
const [value, setValue] = useState(initial);
const valueRef = useRef(value);
valueRef.current = value;
return [value, setValue, valueRef];
}
const IntervalExample = () => {
const [seconds, setSeconds, refSeconds] = useStateAndRef(0);
useEffect(() => {
console.log("useEffect[seconds] executing");
const interval1 = setInterval(() => {
setSeconds((seconds) => seconds + 1);
}, 1000);
const interval2 = setInterval(() => {
// this retrieves only the initial state value of 'seconds'
console.log("interval2: seconds elapsed", refSeconds.current);
}, 1000);
const interval3 = setInterval(function () {
// this retrieves only the initial state value of 'seconds'
console.log("interval3: seconds elapsed", refSeconds.current);
}, 1000);
return () => {
clearInterval(interval1);
clearInterval(interval2);
clearInterval(interval3);
};
}, [seconds]);
return (
<div className="App">
<header className="App-header">
{seconds} seconds have elapsed since mounting.
</header>
</div>
);
};
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<IntervalExample />
</div>
);
}
Updated sandbox without dependencies in useEffect
How can I stop the both the timer when my button is clicked in reactjs.
I have noticed that when my timer is running my whole component is re-rendering every-time how to avoid this part.
export default function App() {
const [counterSecond, setCounterSecond] = React.useState(0);
const [counter, setCounter] = React.useState(120);
const [time, setTime] = React.useState("");
React.useEffect(() => {
setTimeout(() => setCounterSecond(counterSecond + 1), 1000);
setTimeout(() => setCounter(counter - 1), 1000);
}, [counterSecond , counter]);
const handletimer = () => {
setTime(counterSecond);
};
return (
<div className="App">
<div>Countdown: {counterSecond}</div>
<div>Countdown Reverse: {counter}</div>
<div>time: {time} </div>
<button onClick={handletimer}>Submit</button>
</div>
);
}
The best way is to add a state variable representing the status of work. ie: 'working', 'paused' and toggle it.
Also, you need to unsubscribe from timeout to avoid state updates if the component get unmounted.
here is an example where you can stop and resume the timers:
export default function App() {
const [counterSecond, setCounterSecond] = React.useState(0);
const [counter, setCounter] = React.useState(120);
const [time, setTime] = React.useState("");
const [status, setStatus] = React.useState("working");
React.useEffect(() => {
let secondCounterId;
let counterId;
if (status === "working") {
secondCounterId = setTimeout(
() => setCounterSecond(counterSecond + 1),
1000
);
counterId = setTimeout(() => setCounter(counter - 1), 1000);
}
return () => {
clearTimeout(counterId);
clearTimeout(secondCounterId);
};
}, [counterSecond, counter, status]);
const handletimer = () => {
setTime(counterSecond);
};
const stopTimers = () => {
setStatus("paused");
};
const resume = () => {
setStatus("working");
};
return (
<div className="App">
<div>Countdown: {counterSecond}</div>
<div>Countdown Reverse: {counter}</div>
<div>time: {time} </div>
<button onClick={handletimer}>Submit</button>
<button onClick={stopTimers}>Stop</button>
<button onClick={resume}>resume</button>
</div>
);
}
And a working codesandbox
You can create a timerRunning (boolean) variable to check if the timer should run in the useEffect() like this:
const [timerRunning, setTimerRunning] = React.useState(true);
React.useEffect(() => {
if (timerRunning) {
setTimeout(() => setCounterSecond(counterSecond + 1), 1000);
setTimeout(() => setCounter(counter - 1), 1000);
}
}, [counterSecond , counter, timerRunning]);
Then toggle the timerRunning in the handletimer:
const handletimer = () => {
setTimerRunning(false);
// ... other logic
};
The reason time is running because after each render useEffect() will be called.Hence the time. So to correct it, you can set like if "time" is in initialstate then do those functionality otherwise not. So after rendering time will be set set to new time and problem will be solved.
How can I show countdown timer in minute and seconds. right now I am able to show the timer in seconds, only but I want to display both minutes and seconds both.
Currently my countdown timer is showing in this way Countdown: 112 but I want it to be like Countdown: 1: 52
import React from "react";
export default function App() {
const [counter, setCounter] = React.useState(120);
React.useEffect(() => {
counter > 0 && setTimeout(() => setCounter(counter - 1), 1000);
}, [counter]);
return (
<div className="App">
<div>Countdown: {counter === 0 ? "Time over" : counter}</div>
</div>
);
}
Here's a complete solution with formatting time:
// Prepend `0` for one digit numbers. For that the number has to be
// converted to string, as numbers don't have length method
const padTime = time => {
return String(time).length === 1 ? `0${time}` : `${time}`;
};
const format = time => {
// Convert seconds into minutes and take the whole part
const minutes = Math.floor(time / 60);
// Get the seconds left after converting minutes
const seconds = time % 60;
//Return combined values as string in format mm:ss
return `${minutes}:${padTime(seconds)}`;
};
export default function App() {
const [counter, setCounter] = React.useState(120);
React.useEffect(() => {
let timer;
if (counter > 0) {
timer = setTimeout(() => setCounter(c => c - 1), 1000);
}
return () => {
if (timer) {
clearTimeout(timer);
}
};
}, [counter]);
return (
<div className="App">
{counter === 0 ? "Time over" : <div>Countdown: {format(counter)}</div>}
</div>
);
}
A few notes about your original code:
Since the next value of counter depends on the previous one it's better to use the functional form of setState.
It's a good practice to clear timeout when component unmounts.
Import hooks from import stage:
Hooks react
import React, { useState, useEffect } from "react";
export default function App() {
const [counter, setCounter] = useState(120);
useEffect(() => {
counter > 0 && setTimeout(() => setCounter(counter - 1), 1000);
}, [counter]);
return (
<div className="App">
<div>Countdown: {counter === 0 ? "Time over" : counter}</div>
</div>
);
}
Changing your setCounter method to following should work.
React.useEffect(() => {
counter !== 'Time Over' && setTimeout(() => setCounter(counter > 1 ? counter - 1 : 'Time Over'), 1000);
}, [counter]);
This can be done like so:
import React from "react";
export default function App() {
const [counter, setCounter] = React.useState(120);
React.useEffect(() => {
counter > 0 && setTimeout(() => setCounter(counter - 1), 1000);
}, [counter]);
return (
<div className="App">
<div>Countdown: {counter === 0 ? "Time over" : counter}</div>
</div>
);
}