2

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 3

1

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]);
Sign up to request clarification or add additional context in comments.

3 Comments

I am thinking I can use option 1 with a change, the second useEffect could be replaced with a simple setTimeout to run functionX at the right time. Also, is the first useEffect is needed really, can't setInterval be done outside of useEffect i
@mehran You don't want/need to call functionX when the timer hits 0? Sure, you can enqueue calling functionX in a timeout at that point, it should still work I guess, though you'd still want to use the useEffect hook to check the state when it updates. Keep in mind that you want these side-effects to be intentional side-effects, initiated as part of the React component lifecycle.
@mehran If you didn't initiate the setInterval within the useEffect hook callback then I don't know where else you would start your interval. You certainly would not just instantiate the interval in the React function component body as this would be an unintentional side-effect and run each render cycle. You could initiate interval outside the component, but then the state updater wouldn't be in scope.
-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

Comments

-1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.