How do you use two setIntervals with different timers in a React component?

Viewed 65

So right now I have a verifyScreen which users are sent to when they don't have a verified email. I have a setInterval function that runs every 5 seconds to see if the user has confirmed their email.

I also need to have a second setInterval for the resend email button in case a user has not received it. It counts down by 1 until it hits 0 and the user can resend an email.

If I have one or the other in my useEffect everything works fine. They also won't run together if the other is running so I need some help understanding how setInterval works behind the scenes and how I can fix my issue. Thank you.

  const [emailTimer, setEmailTimer] = useState(0);
  const [, setUser] = useAtom(userAtom);

  useEffect(() => {
    const userTimer = setInterval(async () => {
      const refreshedUser = auth().currentUser;
      console.log('checked user');

      if (refreshedUser?.emailVerified) {
        clearInterval(userTimer);
        setUser(refreshedUser);
      } else {
        await auth().currentUser?.reload();
      }
    }, 5000);

    const resendEmailTimer = setInterval(() => {
      if (emailTimer <= 0) {
        clearInterval(resendEmailTimer);
      } else {
        console.log('second subtracted');
        setEmailTimer(emailTimer - 1);
      }
    }, 1000);

    return () => {
      clearInterval(userTimer);
      clearInterval(resendEmailTimer);
    };
  }, [emailTimer]);

If you know the solution and can also explain to me the why behind all this, I would really appreciate that.

Edit, my component which sets the email timer:

   <View style={{ paddingBottom: 10 }}>
     <PrimaryButton
       disabled={emailTimer > 0}
       onPress={async () => {
         setEmailTimer(60);
         await handleSendEmailConfirmation();
       }}>
       <CustomText
         text={emailTimer > 0 ? `${emailTimer}` : 'Resend email'}
         color={theme.colors.white}
       />
     </PrimaryButton>
   </View>
   <OutlinedButton
     onPress={async () => {
       await handleLogOut();
     }}>
     <CustomText text="Cancel" />
   </OutlinedButton>
  </View>

Edit:

What happens with the code right now:

The component renders, and the user interval runs every ~5 seconds. I click on the resend email button, which triggers the email timer every 1 second. While the email counter is going, the user counter pauses until it hits 0. Then the user counter resumes.

Console logs for more description:

enter image description here

Edit 2: Ok I managed to get both intervals working at the same time by moving the user interval outside of the useEffect. This way the email interval triggers when the state updates and it's added to the dependency array.

The one problem is the user interval that is outside of the useEffect sometimes triggers twice in the console. Not sure if that's because I'm running on a simulator or an error.

  const [emailTimer, setEmailTimer] = useState(0);
  const [, setUser] = useAtom(userAtom);

  const userTimer = setInterval(async () => {
    const refreshedUser = auth().currentUser;
    console.log('checked user');

    if (refreshedUser?.emailVerified) {
      clearInterval(userTimer);
      setUser(refreshedUser);
    } else {
      await auth().currentUser?.reload();
    }
  }, 10000);

  useEffect(() => {
    const resendEmailTimer = setInterval(() => {
      if (emailTimer <= 0) {
        clearInterval(resendEmailTimer);
      } else {
        console.log('second subtracted');
        setEmailTimer(emailTimer - 1);
      }
    }, 1000);

    return () => {
      clearInterval(userTimer);
      clearInterval(resendEmailTimer);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [emailTimer]);
1 Answers

If you have two setIntervals, they work totally independently. Of course, if both intervals change the same variable, both will try to update the variable at the same time and sometimes, it could cause an unexpected result, but that's not your case.

One huge issue your code has is, you're creating an interval every time setUser or emailTimer gets updated. setUser will not change after the initialization but emailTimer gets updated frequently and thus will cause hundreds of setIntervals to be created. If it works correctly, that's a miracle. I'm not sure how it could work when you have one interval.

Correct approach is adding them one time when the page mounts:

useEffect(() => {
  const userTimer = setInterval(...);
  const resendEmailTimer = setInterval(...);
}, []); // this empty [] is the key

ESLint may complain about react-hooks/exhaustive-deps rule but it doesn't make sense in your case, so simply suppress it. There's a lot of debate on whether this rule should be strictly enforced or not, but that's another topic.

Side note: I don't like the naming of your emailTimer variable. All other intervals have xxxTimer format, and it could easily mislead emailTimer is also an interval, so maybe consider renaming your state variable to emailTimerCount.

Related