Reputation:
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>
)
}
Upvotes: 2
Views: 307
Reputation: 51856
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>
Upvotes: 1
Reputation: 29282
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.
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>
Upvotes: 1
Reputation: 171
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]);
Upvotes: 0