user9445985
user9445985

Reputation:

Why I'm not able to clear my setInterval?

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

Answers (3)

Patrick Roberts
Patrick Roberts

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

Yousaf
Yousaf

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.

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>

Upvotes: 1

Nahuel
Nahuel

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

Related Questions