javascriptreactjsreact-hooksrequestanimationframe

Bug on a timer made with requestAnimationFrame


I made a timer for a card game and everything was working fine to this point. But I wanted to stop the timer when there's a winner and stop the timer animation by the way. So basically with the following code the when there's a winner the timer will only stop at the end of the current turn before going back to idle state (when value is null).

useEffect(() => {

    if (turnTimerStartedAt && !winnerPlayerId) {
      const startedAt =
        Date.now() - (Rune.gameTimeInSeconds() - turnTimerStartedAt) * 1000;

      const tick = () => {
        const newValue = MAX_TURN_TIME - (Date.now() - startedAt) / 1000 - 0.5;
        if (newValue <= 0) {
          setValue(0);
        } else {
          setValue(newValue);
          requestAnimationFrame(tick);
        }
      };
      tick();
    } else {
      setValue(null);
    }

}, [turnTimerStartedAt, winnerPlayerId]);

I want it to stop it as soon as a winnerPlayerId is set and noticed it doesn't stop when any of the variables (turnTimerStartedAt or winnerPlayerId) make the condition falsy.

Is there a way to make the timer stop when the dependencies change ?


Solution

  • You need to cancel the requestAnimationFrame by assigning the requestId to a variable, and cancelling the request using cancelAnimationFrame.

    In this case, I would use the cleanup function of useEffect to cancel the animation frame, whenever the dependencies change:

    useEffect(() => {
      let requestId;
      
      if (turnTimerStartedAt && !winnerPlayerId) {
        const startedAt =
          Date.now() - (Rune.gameTimeInSeconds() - turnTimerStartedAt) * 1000;
        const tick = () => {
          const newValue = MAX_TURN_TIME - (Date.now() - startedAt) / 1000 - 0.5;
          if (newValue <= 0) {
            setValue(0);
          } else {
            setValue(newValue);
            requestId = requestAnimationFrame(tick); // set the requestId
          }
        };
        tick();
      } else {
        setValue(null);
      }
      
      return () => {
        cancelAnimationFrame(requestId); // cancel the animation frame
      };
    }, [turnTimerStartedAt, winnerPlayerId]);