2
export default function Timer() {
   const [timer, setTimer] = useState(0)

   const checkTimer = () => {
     console.log(timer);
   }

   useEffect(() => {
      const timer = setInterval(() => {
        setTimer(prevCount => prevCount + 1);
      }, 1000);

      startProgram(); //This starts some other functions

      return () => {
        checkTimer();
        clearInterval(timer);
      }
   }, [])
}

Above is a simplified version of my code and the main issue - I am trying to increase the timer state by setting an interval in useEffect() (only once). However, in checkTimer() the value is always 0, even though the console statement execute every second. I am new to reactjs and would appreciate some help as this is already taking me too many hours to fix.

Ajeet Shah
  • 18,551
  • 8
  • 57
  • 87
feedy
  • 1,071
  • 5
  • 17
  • Try creating a custom hook like `useInterval` as shown in demo of [this post](https://stackoverflow.com/a/66804905/2873538). This hook will be better than dealing with `setTimeout`. – Ajeet Shah Mar 25 '21 at 18:29

1 Answers1

2

checkTimer is showing you the initial value of timer state because of stale closure. That means at the time when useEffect was executed, (i.e. once at component mount due to [] as dependency), it registered a cleanup function which created a closure around checkTimer function (and everything, state or props values, it uses). And when this closure was created the value of timer state was 0. And it will always remain that.

There are few options to fix it.

One quick solution would be to use useRef:

const timer = useRef(0);

const checkTimer = () => {
  console.log(timer.current);
};

useEffect(() => {
  const id = setInterval(() => {
    timer.current++;
  }, 1000);

  return () => {
    checkTimer();
    clearInterval(id);
  };
}, []);

Check this related post to see more code examples to fix this.


Edit:

And, if you want to show the timer at UI as well, we need to use state as we know that "ref" data won't update at UI. So, the option 2 is to use "updater" form of setTimer to read the latest state data in checkTimer function:

const [timer, setTimer] = useState(0);

const checkTimer = () => {
  let prevTimer = 0;
  setTimer((prev) => {
    prevTimer = prev; // HERE
    return prev; // Returning same value won't change state data and won't causes a re-render
  });
  console.log(prevTimer);
};

useEffect(() => {
  const id = setInterval(() => {
    setTimer((prev) => prev + 1);
  }, 1000);

  return () => {
    checkTimer();
    clearInterval(id);
  };
}, []);
Ajeet Shah
  • 18,551
  • 8
  • 57
  • 87
  • 1
    Thank you so much! This worked perfectly and it finally makes sense in my head – feedy Mar 17 '21 at 18:59
  • There is one thing to keep in mind with this solution: if you are showing `timer` state value somewhere at UI, it won't update. Because `useRef` doesn't participate in rerendering. But if you are just using it for "counter", this is good. – Ajeet Shah Mar 17 '21 at 19:01
  • 1
    I am only using it as counter, thanks! Out of curiosity, would it update on the UI with my original code posted above? And if I wanted to achieve both (counting current value and displaying it), would combining both be acceptable solution or there is some much better way? Also, please feel free to not get into it if it will take too much time to explain. I am just getting into React and I will eventually get there myself, just this issue took me hours and I couldn't avoid asking for help anymore :D – feedy Mar 17 '21 at 19:04
  • See my edit. The options 2 will update the UI as well, as we came back to state data. There is another option 3, it is using "timer" as ref variable and a boolean flag in state to force render. Just change that boolean state value and UI will show updated value from "ref" too. – Ajeet Shah Mar 17 '21 at 19:18
  • If you combine both ways : "ref" and "state" for timer. You just need to be careful that you update both whenever you do. (Seems buggy to me if we forget to update both.) – Ajeet Shah Mar 17 '21 at 19:25