skip to Main Content

Consider this code:

const [seconds, setSeconds] = useState<number>(START_VALUE);

useEffect(() => {
  const intervalId = setInterval(() => {
    setSeconds((previousSeconds) => previousSeconds - 1);
  }, 1000);

  if (seconds <= 0) {
    clearInterval(intervalId);
    functionX();
  }

  return () => clearInterval(intervalId);
}, [seconds]);

The problem with the above code is that every second the useEffect gets triggered, is there any way to access the value out of setSeconds calculation to be used inside setInterval?

3

Answers


  1. The below post recommends a Ref

    setInterval call is open in the useEffect. It gets invoked for every change in the state. Therefore it goes infinite. This is the issue with the current code.

    Please put a conditional for setInterval so that it gets invoked only once. Since it is a scheduling code, it should get invoked only once. Once it gets scheduled, it works automatically on the schedules.

    In order to put a conditional for setInterval, please use a ref as shown in the sample code below. As you know, Ref will retain its value during renders. Therefore it would be an ideal choice here.
    For more about its usage, please see here When to use refs

    A clean up code for useEffect is not required in this case, since the custom code inside useEffect takes care of it. It clears the interval set.

    App.js

    import { useState, useEffect, useRef } from 'react';
    
    const START_VAL = 3;
    
    export default function App() {
      const [seconds, setSeconds] = useState(START_VAL);
      const intervalId = useRef(0);
    
      useEffect(() => {
        if (intervalId.current == 0) {
          intervalId.current = setInterval(() => {
            setSeconds((previousSeconds) => previousSeconds - 1);
          }, 1000);
        } else if (seconds === 0) {
          clearInterval(intervalId.current);
        }
      }, [seconds]);
      return `${seconds} of ${START_VAL} seconds`;
    }
    

    Test run

    The component renders 4 times in total.

    // 3 of 3 seconds
    // 2 of 3 seconds
    // 1 of 3 seconds
    // 0 of 3 seconds
    
    Login or Signup to reply.
  2. The problem with the above code is that every second the useEffect
    gets triggered

    Your useEffect hook is trying to do too much. You can split the code/logic up into as many logical effect as is necessary for your use case. In any case you will have at least 1 useEffect hook being called each time the seconds state updates so the code can correctly check when it reaches 0.

    is there any way to access the value out of setSeconds calculation
    to be used inside setInterval?

    No, not really. In doing so you will break a few design patterns.

    • The setInterval callback creates a Javascript closure over lexical values in scope when it is created, so the callback can’t see the updated seconds state value when it is called.
    • You could check the current state value in the setSeconds callback, but since React state updater functions are to be considered pure functions, you can’t call functionX or cancel the interval timer from that scope.

    Trivially you have at least a couple of options:

    Use Two useEffect Hooks to Control Interval Timer

    Use one useEffect hook with empty dependency array to initiate the interval and clean up any running intervals in the event of component unmounting, and another useEffect hook to handle the side-effect of checking when the timer expires and invoking functionX. Use a React ref to hold a reference to the timer id.

    const [seconds, setSeconds] = useState<number>(START_VALUE);
    const timerRef = useRef<number | null>(null);
    
    useEffect(() => {
      timerRef.current = setInterval(() => {
        setSeconds((seconds) => seconds - 1);
      }, 1000);
    
      return () => {
        if (timerRef.current) {
          clearInterval(timerRef.current);
        }
      };
    }, []);
    
    useEffect(() => {
      if (timerRef.current && seconds <= 0) {
        clearInterval(timerRef.current);
        timerRef.current = null;
        functionX();
      }
    }, [seconds]);
    

    Use One useEffect Hook to Control Timeout Timer

    Use a single useEffect hook and a setTimeout timer instead of an interval, so the effect running triggers the next timer iteration. This avoids creating extraneous unnecessary intervals your original code was doing.

    const [seconds, setSeconds] = useState<number>(START_VALUE);
    
    const functionX = () => console.log("timer expired");
    
    useEffect(() => {
      const intervalId = setTimeout(() => {
        setSeconds((seconds) => seconds - 1);
      }, 1000);
    
      if (seconds <= 0) {
        clearTimeout(intervalId);
        functionX();
      }
      return () => {
        clearTimeout(intervalId);
      }
    }, [seconds]);
    
    Login or Signup to reply.
  3. This post recommends setTimeout instead of setInterval

    The reasoning is this : useEffect and a state setter invocation within it itself will automatically forms a loop. In fact, it is an infinite loop unless an exit is provided. Therefore developing another loop through setInterval may be a redundant code. The point is we need to place an exit suiting to the logic. The sample code below does the same.

    Notes: The setTimeout over here invokes the state setter one every second. Since the effect is depending on the state, the previous change will itself invoke another setTimeout. The same will repeat until the exit condition meets.

    App.js

    import { useState, useEffect, useRef } from 'react';
    const START_VAL = 3;
    
    export default function App() {
      const [seconds, setSeconds] = useState(START_VAL);
    
      useEffect(() => {
        seconds > 0
          ? setTimeout(() => {
              setSeconds((previousSeconds) => previousSeconds - 1);
            }, 1000)
          : null;
      }, [seconds]);
      return `${seconds} of ${START_VAL} seconds`;
    }
    

    Test run

    The component renders 4 times in total.

    // 3 of 3 seconds
    // 2 of 3 seconds
    // 1 of 3 seconds
    // 0 of 3 seconds
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search