Why I'm not able to clear my setInterval?

Viewed 185

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>
  )
}
3 Answers

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]);
Related