In my code I have some problems with intervals. I have situation like this
import { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
function handleClick() {
const timer = setInterval(() => {
if (count >= 10) {
clearInterval(timer);
} else {
setCount((prevCount) => prevCount + 1);
}
}, 1000);
}
return (
<>
<button onClick={handleClick}>start</button>
<p>{count}</p>
</>
);
}
export default App;
what am trying to do is to start an interval when user click a button and stop it when counter reaches 10, but it never stops and debugger says that inside setInterval count is always 0. Does anybody know what is the problem? I also found that if I rewrite component to class component like this
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
handleClick() {
let myInterval = setInterval(() => {
if (this.state.count >= 10) {
clearInterval(myInterval);
} else {
this.setState((prevState) => ({
count: prevState.count + 1,
}));
}
}, 1000);
}
render() {
return (
<>
<button onClick={this.handleClick.bind(this)}>start</button>
<p>{this.state.count}</p>
</>
);
}
}
it works perfectly.
I don't know what's happening and I spend almost day trying to figure out.
So does anybody know why class component works and why functional component doesn't?
Thanks in advance
You're seeing a stale count in the setInterval callback function since it captures the value at the time you're starting the timer.
You can use the "ref boxing" pattern to have a readable reference to the latest value of the state atom:
import {useState, useRef} from 'react';
function App() {
const [count, setCount] = useState(0);
const countRef = useRef(null);
countRef.current = count; // Update the boxed value on render
function handleClick() {
const timer = setInterval(() => {
if (countRef.current >= 10) { // Read the boxed value
clearInterval(timer);
} else {
setCount((prevCount) => prevCount + 1);
}
}, 1000);
}
return (
<>
<button onClick={handleClick}>start</button>
<p>{count}</p>
</>
);
}
Note that you're currently not correctly cleaning up the timer on unmount, leading to React warning you about that when you'd unmount your app. The same holds for your class component.
I would like to setup a counter which can be paused as well as resumed in React.js. But whatever I have tried so far is working the functionality part (pause/resume is working) but it's not updating the counter on render. Below is my code:
const ProgressBar = (props) => {
const [isPlay, setisPlay] = useState(false);
const [progressText, setProgressText] = useState(
props.duration ? props.duration : 20
);
var elapsed,
secondsLeft = 20;
const timer = () => {
// setInterval for every second
var countdown = setInterval(() => {
// if allowed time is used up, clear interval
if (secondsLeft < 0) {
clearInterval(countdown);
return;
}
// if paused, record elapsed time and return
if (isPlay === true) {
elapsed = secondsLeft;
return;
}
// decrement seconds left
secondsLeft--;
console.warn(secondsLeft);
}, 1000);
};
timer();
const stopProgress = () => {
setisPlay(!isPlay);
if (isPlay === false) {
secondsLeft = elapsed;
}
};
return (
<>
<p>{secondsLeft}</p>
</>
);
};
export default ProgressBar;
I have tried React.js state, global var type, global let type, react ref so far to make the variable global but none of them worked..
So basically why does your example not work?
Your secondsLeft variable not connected to your JSX. So each time your component rerendered it creates a new secondsLeft variable with a value of 20 (Because rerendering is simply the execution of your function that returns JSX)
How to make your variable values persist - useState or useReducer hook for react functional component or state for class based one. So react will store all the values for you for the next rerender cycle.
Second issue is React doesn't rerender your component, it just doesn't know when it should. So what causes rerendering of your component -
Props change
State change
Context change
adding/removing your component from the DOM
Maybe I missing some other cases
So example below works fine for me
import { useEffect, useState } from "react";
function App() {
const [pause, setPause] = useState(false);
const [secondsLeft, setSecondsLeft] = useState(20);
const timer = () => {
var countdown = setInterval(() => {
if (secondsLeft <= 0) {
clearInterval(countdown);
return;
}
if (pause === true) {
clearInterval(countdown);
return;
}
setSecondsLeft((sec) => sec - 1);
}, 1000);
return () => {
clearInterval(countdown);
};
};
useEffect(timer, [secondsLeft, pause]);
const pauseTimer = () => {
setPause((pause) => !pause);
};
return (
<div>
<span>Seconds Left</span>
<p>{secondsLeft}</p>
<button onClick={pauseTimer}>{pause ? "Start" : "Pause"}</button>
</div>
);
}
import React, { useState, useEffect } from "react";
import logo from "./logo.svg";
import "./App.css";
var timer = null;
function App() {
const [counter, setCounter] = useState(0);
const [isplayin, setIsPlaying] = useState(false);
const pause = () => {
setIsPlaying(false);
clearInterval(timer);
};
const reset = () => {
setIsPlaying(false);
setCounter(0);
clearInterval(timer);
};
const play = () => {
setIsPlaying(true);
timer = setInterval(() => {
setCounter((prev) => prev + 1);
}, 1000);
};
return (
<div className="App">
<p>Counter</p>
<h1>{counter}</h1>
{isplayin ? (
<>
<button onClick={() => pause()}>Pause</button>
<button onClick={() => reset()}>Reset</button>
</>
) : (
<>
{counter > 0 ? (
<>
<button onClick={() => play()}>Resume</button>
<button onClick={() => reset()}>Reset</button>
</>
) : (
<button onClick={() => play()}>Start</button>
)}
</>
)}
</div>
);
}
export default App;
I'm trying to create an automatic counter when the component(Home here for instance) renders in the view. The counter should start at 1 and stop at 10. But I'm not able to clear my setInterval at 10. The counter remains incrementing.
What is the problem?
Thanks a lot.
export const Home = () => {
const [counter, setCounter] = useState(1)
let timer = useRef()
const animate = () => {
if(counter === 10){
clearInterval(timer.current)
}else{
setCounter((previous) => previous + 1)
}
}
useEffect(() => {
timer.current = window.setInterval(() => {
animate()
}, 900)
}, [])
return (
<div style={{textAlign: "center"}}>
<h1>{counter}</h1>
</div>
)
}
Problem is the closure of animate function over the initial value of counter, i.e. 1. As a result, the condition counter === 10 never evaluates to true.
This is why you should never lie about the dependencies of the useEffect hook. Doing so can lead you to bugs because of closures over the stale values from the previous renders of your component.
Solution
You can get rid of animate() function and write the logic inside the useEffect hook.
function App() {
const [counter, setCounter] = React.useState(1);
const counterRef = React.useRef(counter);
React.useEffect(() => {
const id = setInterval(() => {
// if "counter" is 10, stop the interval
if (counterRef.current === 10) {
clearInterval(id);
} else {
// update the "counter" and "counterRef"
setCounter((prevCounter) => {
counterRef.current = ++prevCounter;
return prevCounter;
});
}
}, 900);
// clearn interval on unmount
return () => clearInterval(id);
}, []);
return (
<div style={{ textAlign: "center" }}>
<h1>{counter}</h1>
</div>
);
}
ReactDOM.render(<App/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I'd create a second useEffect() for clearing the interval:
const { useEffect, useRef, useState } = React;
const Home = () => {
const [counter, setCounter] = useState(1);
const timer = useRef();
useEffect(() => {
timer.current = setInterval(() => {
setCounter(x => x + 1);
}, 900);
// in case the component unmounts before the timer finishes
return () => clearInterval(timer.current);
}, []);
useEffect(() => {
if (counter === 10) {
clearInterval(timer.current);
}
}, [counter]);
return (
<div style={{ textAlign: 'center' }}>
<h1>{counter}</h1>
</div>
);
}
ReactDOM.render(<Home/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
That's because modifying ref.current doesn't trigger a rerender.
You need to use useEffect to clear it.
const animate = () => {
setCounter((previous) => previous + 1);
};
useEffect(() => {
if (counter === 10) clearInterval(timer.current);
}, [counter]);
i'm trying to do countdown timer with react. It will be basically countdown from 10 to 0 and when 0 i will call some function.
i found ideally for me some example: https://codesandbox.io/s/0q453m77nw?from-embed
but it's a class component i wan't to do that with functional component and hooks but i can't.
i tried:
function App() {
const [seconds, setSeconds] = useState(10);
useEffect(() => {
setSeconds(setInterval(seconds, 1000));
}, []);
useEffect(() => {
tick();
});
function tick() {
if (seconds > 0) {
setSeconds(seconds - 1)
} else {
clearInterval(seconds);
}
}
return (
<div className="App">
<div
{seconds}
</div>
</div>
);
}
export default App;
it's count down from 10 to 0 very quickly not in 10 seconds.
where i mistake ?
It appears the multiple useEffect hooks are causing the countdown to run more than once per second.
Here's a simplified solution, where we check the seconds in the useEffect hook and either:
Use setTimeout to update seconds after 1 second, or
Do something else (the function you want to call at the end of the countdown)
There are some downsides to this method, see below.
function App() {
const [seconds, setSeconds] = React.useState(10);
React.useEffect(() => {
if (seconds > 0) {
setTimeout(() => setSeconds(seconds - 1), 1000);
} else {
setSeconds('BOOOOM!');
}
});
return (
<div className="App">
<div>
{seconds}
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'))
<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>
Downsides
Using setInterval has the downside that it could be stopped - for example, the component is unmounted, you navigate to a different tab, or close your computer. If the timer requires more robustness, the better alternative would be to store an endTime in the state (like a global store or context) and have your component check the current time against the endTime to calculate the countdown.
Do you care about precision? If so, you don't want setInterval. If you don't care about precision (and you probably don't) then you can schedule a call to tick() on an interval, not the other way around.
const TimeoutComponent extends Component {
constructor(props) {
super(props);
this.state = { countdown: 10 };
this.timer = setInterval(() => this.tick(), props.timeout || 10000);
}
tick() {
const current = this.state.countdown;
if (current === 0) {
this.transition();
} else {
this.setState({ countdown: current - 1 });
}
}
transition() {
clearInterval(this.timer);
// do something else here, presumably.
}
render() {
return <div className="timer">{this.state.countDown}</div>;
}
}
This depends on your logic a little bit. In the current situation your useEffect where you run your tick method is running on every render. You can find a naive example below.
function App() {
const [seconds, setSeconds] = useState(10);
const [done, setDone] = useState(false);
const foo = useRef();
useEffect(() => {
function tick() {
setSeconds(prevSeconds => prevSeconds - 1)
}
foo.current = setInterval(() => tick(), 1000)
}, []);
useEffect(() => {
if (seconds === 0) {
clearInterval(foo.current);
setDone(true);
}
}, [seconds])
return (
<div className="App">
{seconds}
{done && <p>Count down is done.</p>}
</div>
);
}
In the first effect we are doing the countdown. Using callback one for setting state since interval creates a closure. In the second effect we are checking our condition.
Simply use this snippet, As it will also help to memoize the timeout callback.
const [timer, setTimer] = useState(60);
const timeOutCallback = useCallback(() => setTimer(currTimer => currTimer - 1), []);
useEffect(() => {
timer > 0 && setTimeout(timeOutCallback, 1000);
}, [timer, timeOutCallback]);
console.log(timer);
Hope this will help you or somebody else.
Happy Coding!
I'm trying out the new React Hooks and have a Clock component with a time value which is supposed to increase every second. However, the value does not increase beyond one.
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time + 1);
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
The reason is because the callback passed into setInterval's closure only accesses the time variable in the first render, it doesn't have access to the new time value in the subsequent render because the useEffect() is not invoked the second time.
time always has the value of 0 within the setInterval callback.
Like the setState you are familiar with, state hooks have two forms: one where it takes in the updated state, and the callback form which the current state is passed in. You should use the second form and read the latest state value within the setState callback to ensure that you have the latest state value before incrementing it.
Bonus: Alternative Approaches
Dan Abramov goes in-depth into the topic about using setInterval with hooks in his blog post and provides alternative ways around this issue. Highly recommend reading it!
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(prevTime => prevTime + 1); // <-- Change this line!
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
As others have pointed out, the problem is that useState is only called once (as deps = []) to set up the interval:
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time + 1);
}, 1000);
return () => window.clearInterval(timer);
}, []);
Then, every time setInterval ticks, it will actually call setTime(time + 1), but time will always hold the value it had initially when the setInterval callback (closure) was defined.
You can use the alternative form of useState's setter and provide a callback rather than the actual value you want to set (just like with setState):
setTime(prevTime => prevTime + 1);
But I would encourage you to create your own useInterval hook so that you can DRY and simplify your code by using setInterval declaratively, as Dan Abramov suggests here in Making setInterval Declarative with React Hooks:
function useInterval(callback, delay) {
const intervalRef = React.useRef();
const callbackRef = React.useRef(callback);
// Remember the latest callback:
//
// Without this, if you change the callback, when setInterval ticks again, it
// will still call your old callback.
//
// If you add `callback` to useEffect's deps, it will work fine but the
// interval will be reset.
React.useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// Set up the interval:
React.useEffect(() => {
if (typeof delay === 'number') {
intervalRef.current = window.setInterval(() => callbackRef.current(), delay);
// Clear interval if the components is unmounted or the delay changes:
return () => window.clearInterval(intervalRef.current);
}
}, [delay]);
// Returns a ref to the interval ID in case you want to clear it manually:
return intervalRef;
}
const Clock = () => {
const [time, setTime] = React.useState(0);
const [isPaused, setPaused] = React.useState(false);
const intervalRef = useInterval(() => {
if (time < 10) {
setTime(time + 1);
} else {
window.clearInterval(intervalRef.current);
}
}, isPaused ? null : 1000);
return (<React.Fragment>
<button onClick={ () => setPaused(prevIsPaused => !prevIsPaused) } disabled={ time === 10 }>
{ isPaused ? 'RESUME ⏳' : 'PAUSE 🚧' }
</button>
<p>{ time.toString().padStart(2, '0') }/10 sec.</p>
<p>setInterval { time === 10 ? 'stopped.' : 'running...' }</p>
</React.Fragment>);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
body,
button {
font-family: monospace;
}
body, p {
margin: 0;
}
p + p {
margin-top: 8px;
}
#app {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
button {
margin: 32px 0;
padding: 8px;
border: 2px solid black;
background: transparent;
cursor: pointer;
border-radius: 2px;
}
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
Apart from producing simpler and cleaner code, this allows you to pause (and clear) the interval automatically by simply passing delay = null and also returns the interval ID, in case you want to cancel it yourself manually (that's not covered in Dan's posts).
Actually, this could also be improved so that it doesn't restart the delay when unpaused, but I guess for most uses cases this is good enough.
If you are looking for a similar answer for setTimeout rather than setInterval, check this out: https://stackoverflow.com/a/59274757/3723993.
You can also find declarative version of setTimeout and setInterval, useTimeout and useInterval, a few additional hooks written in TypeScript in https://www.npmjs.com/package/#swyg/corre.
useEffect function is evaluated only once on component mount when empty input list is provided.
An alternative to setInterval is to set new interval with setTimeout each time the state is updated:
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = setTimeout(() => {
setTime(time + 1);
}, 1000);
return () => {
clearTimeout(timer);
};
}, [time]);
The performance impact of setTimeout is insignificant and can be generally ignored. Unless the component is time-sensitive to the point where newly set timeouts cause undesirable effects, both setInterval and setTimeout approaches are acceptable.
useRef can solve this problem, here is a similar component which increase the counter in every 1000ms
import { useState, useEffect, useRef } from "react";
export default function App() {
const initalState = 0;
const [count, setCount] = useState(initalState);
const counterRef = useRef(initalState);
useEffect(() => {
counterRef.current = count;
})
useEffect(() => {
setInterval(() => {
setCount(counterRef.current + 1);
}, 1000);
}, []);
return (
<div className="App">
<h1>The current count is:</h1>
<h2>{count}</h2>
</div>
);
}
and i think this article will help you about using interval for react hooks
An alternative solution would be to use useReducer, as it will always be passed the current state.
function Clock() {
const [time, dispatch] = React.useReducer((state = 0, action) => {
if (action.type === 'add') return state + 1
return state
});
React.useEffect(() => {
const timer = window.setInterval(() => {
dispatch({ type: 'add' });
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((seconds) => {
if (seconds === 5) {
setSeconds(0);
return clearInterval(interval);
}
return (seconds += 1);
});
}, 1000);
}, []);
Note: This will help to update and reset the counter with useState hook. seconds will stop after 5 seconds. Because first change setSecond value then stop timer with updated seconds within setInterval. as useEffect run once.
This solutions dont work for me because i need to get the variable and do some stuff not just update it.
I get a workaround to get the updated value of the hook with a promise
Eg:
async function getCurrentHookValue(setHookFunction) {
return new Promise((resolve) => {
setHookFunction(prev => {
resolve(prev)
return prev;
})
})
}
With this i can get the value inside the setInterval function like this
let dateFrom = await getCurrentHackValue(setSelectedDateFrom);
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time => time + 1);// **set callback function here**
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
Somehow similar issue, but when working with a state value which is an Object and is not updating.
I had some issue with that so I hope this may help someone.
We need to pass the older object merged with the new one
const [data, setData] = useState({key1: "val", key2: "val"});
useEffect(() => {
setData(...data, {key2: "new val", newKey: "another new"}); // --> Pass old object
}, []);
Do as below it works fine.
const [count , setCount] = useState(0);
async function increment(count,value) {
await setCount(count => count + 1);
}
//call increment function
increment(count);
I copied the code from this blog. All credits to the owner. https://overreacted.io/making-setinterval-declarative-with-react-hooks/
The only thing is that I adapted this React code to React Native code so if you are a react native coder just copy this and adapt it to what you want. Is very easy to adapt it!
import React, {useState, useEffect, useRef} from "react";
import {Text} from 'react-native';
function Counter() {
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest function.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
const [count, setCount] = useState(0);
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, 1000);
return <Text>{count}</Text>;
}
export default Counter;
const [loop, setLoop] = useState(0);
useEffect(() => {
setInterval(() => setLoop(Math.random()), 5000);
}, []);
useEffect(() => {
// DO SOMETHING...
}, [loop])
For those looking for a minimalist solution for:
Stop interval after N seconds, and
Be able to reset it multiple times again on button click.
(I am not a React expert by any means my coworker asked to help out, I wrote this up and thought someone else might find it useful.)
const [disabled, setDisabled] = useState(true)
const [inter, setInter] = useState(null)
const [seconds, setSeconds] = useState(0)
const startCounting = () => {
setSeconds(0)
setDisabled(true)
setInter(window.setInterval(() => {
setSeconds(seconds => seconds + 1)
}, 1000))
}
useEffect(() => {
startCounting()
}, [])
useEffect(() => {
if (seconds >= 3) {
setDisabled(false)
clearInterval(inter)
}
}, [seconds])
return (<button style = {{fontSize:'64px'}}
onClick={startCounting}
disabled = {disabled}>{seconds}</button>)
}
Tell React re-render when time changed.opt out
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time + 1);
}, 1000);
return () => {
window.clearInterval(timer);
};
}, [time]);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>