1
function func(){
  const [time, setTime] = useState(10);
  var timeRemaining = 10;
  const myInterval = setInterval(() => {
      if (timeRemaining > 0) {
        timeRemaining = timeRemaining - 1;
        setTime(timeRemaining);
      } else { clearInterval(myInterval) }
    }, 1000);
  return <div>{time}</div>
}

The code above works thanks to the variable timeRemaining. However, it stops working if I remove that variable (in order to keep the code clean):

const myInterval = setInterval(() => {
    if (time> 0) { setTime(time-1); } 
    else { clearInterval(myInterval); }
  }, 1000);

By rewriting it in the above way, it stops updating time.

julianobrasil
  • 8,954
  • 2
  • 33
  • 55
  • `setTime(currentTime => currentTime - 1)`. But honestly, [don't manage time yourself](https://stackoverflow.com/q/20618355/1218980). Use `Date.now()` and do the math using the start time. – Emile Bergeron May 05 '20 at 00:57
  • Also, see: [Will `setInterval` drift?](https://stackoverflow.com/q/985670/1218980) – Emile Bergeron May 05 '20 at 01:38

4 Answers4

4

Use effects to control interval, ref to hold reference to interval timer reference, and functional state update to correctly manage state.

  • Effect 1 - setup (mount) and cleanup (unmount) of interval effect
  • Effect 2 - clears interval when time reaches 0

Functional Component Code:

function App() {
  const timerRef = useRef(null);
  const [time, setTime] = useState(10);

  useEffect(() => {
    // Use pure functional state update to correctly queue up state updates
    // from previous state time value.
    // Store returned interval ref.
    timerRef.current = setInterval(() => setTime(t => t - 1), 1000);

    // Return effect cleanup function
    return () => clearInterval(timerRef.current);
  }, []); // <-- Empty dependency array, effect runs once on mount.

  useEffect(() => {
    // Clear interval and nullify timer ref when time reaches 0
    if (time === 0) {
      clearInterval(timerRef.current);
      timerRef.current = null;
    }
  }, [time]); // <-- Effect runs on mount and when time value updates.

  return <div>{time}</div>;
}

Edit focused-mendeleev-2cezr

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
0

Use the function version of setTime, though I'd suggest putting your interval into a useEffect for cleanup as well

  setTime((time) => {
    if (time > 0) {
      return time - 1;
    }
    clearInterval(myInterval);
    return time;
  });

With a useEffect for cleanup of the interval on unmount:

const {useState,useEffect} = React;    
    
    function Func() {
      const [time, setTime] = useState(10);
      useEffect(() => {
        const myInterval = setInterval(() => {
          setTime((time) => {
            if (time > 0) {
              return time - 1;
            }
            clearInterval(myInterval);
            return time;
          });
        }, 1000);
        return () => {
          clearInterval(myInterval);
        };
      }, []);

      return <div>{time}</div>;
    }
    
 ReactDOM.render(<Func />,document.querySelector('#root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"/>
Zachary Haber
  • 10,376
  • 1
  • 17
  • 31
-1

The problem is that you're creating a new setInterval every time the component was recreated, as a consequence of setTime execution.

As an alternative to the other answers, that put the setInterval calling inside an useEffect, you can also switch to a setTimeout and not using the useEffect:

const App = (props) => {
  const [time, setTime] = useState(10);

  setTimeout(() => {
    if (time > 0) {
      setTime(time - 1);
    }
  }, 1000);

  return <div>{time}</div>;
};

Every time the component is redrawn as an effect of setTime call, the setTimeout will be fired again.

julianobrasil
  • 8,954
  • 2
  • 33
  • 55
  • 1
    This can have issues if the component is ever re-rendered from a parent. – Zachary Haber May 05 '20 at 01:40
  • @ZacharyHaber, could you elaborate it a little bit? I'm very used to angular, but a complete newbie regarding react and I'd like to understand the problem here. BTW, if the explanation is too long for a comment and you can just link to some blog or article talking about it, I'll be glad. – julianobrasil May 05 '20 at 11:22
  • 2
    Essentially the issue is that whenever a component re-renders, it also re-renders it's children. Since you have the setTimeout in the render function, if the App re-renders, then it'll start up a new timer. That new timer will actually setTime to what it would be currently, so react won't keep re-rendering over again. But you can have the timer function run more often than you would expect. https://codesandbox.io/s/timer-fun-rfzeg Play around with this, but keep in mind that StrictMode is on, which will re-render everything twice. – Zachary Haber May 05 '20 at 13:49
  • Thanks for the nice demo! – julianobrasil May 05 '20 at 14:59
  • 2
    The other issue is when the component unmounts for any reason the timeout will expire and try to set state of an unmounted component. React will display a warning for this in non-production builds. You'll want to clean up any asynchronous code when components unmount. – Drew Reese May 05 '20 at 15:08
-1
 function func(){
  const [time, setTime] = useState(10);
  var timeRemaining = 10;
  const myInterval = setInterval(() => {
                    if (timeRemaining > 0) {
                        timeRemaining = timeRemaining - 1;
                        setTime(timeRemaining);
                    } else { clearInterval(myInterval) }
                }, 1000);
   return <div>{time}</div>
 }

Here with every render, JS would start a setInterval. And in setInterval's callback, variable timeRemaining allocated for every render in respect get stored, or we call it CLOSURE. So 1 sec after the very beginning, REACT render func again. And thus you get another interval and a variable timeRemaining. It'll run from timeRemaining = 10.

setTime will always be one thing. And time will be read from HOOKS's pools.

This code is WRONG! You'll have more and more interval with its timeRemaining.

Singhi John
  • 307
  • 2
  • 10