skip to Main Content

so I have a react modal which has a countdown timer. The timer doesn’t seem to work on Chrome but works on Firefox. On Chrome timer starts to count down but decreases its ticking pase when it ticks for about 20 seconds.

This is the current implementation of the modal and the timer.

import React, {useState, useEffect } from 'react';
import BaseModal from './Index';

export default function OtpModal(props: Props) {
  const [timeLeft, setTimeLeft] = useState<number>(120);

  useEffect(() => {
    const intervalId = setInterval(() => {
      console.log('tick', timeLeft);
      setTimeLeft(timeLeft - 1);
    }, 1000);

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

  return (
    <BaseModal>
      <div>
        OTP timeout in{' '}
        <span style={{fontWeight: '500'}}>
          {Math.floor(timeLeft / 60)}:
          {(timeLeft % 60).toString().padStart(2, '0')} 
        </span>
      </div>
    </BaseModal>
  )
}

3

Answers


  1. You are running your useEffect and setting a new timer at each second.

    Maybe it is causing issue if there is timer throttling.

    You can set only one timer and run once your useEffect like that:

    useEffect(() => {
        const intervalId = setInterval(() => {
          setTimeLeft((prev) => (prev > 0 ? prev - 1 : 0));
        }, 1000);
    
        return () => {
          clearInterval(intervalId);
        };
      }, []);
    
    Login or Signup to reply.
  2. When we run clearInterval and setInterval, their timing shifts. If we re-render and re-apply effects too often, the interval never gets a chance to fire!

    I would recommend studying this blog post about problems with the setInterval and react hooks. The blog post suggests creating a custom hook(useInterval) as follows:

    import React, { useState, useEffect, useRef } from 'react';
    
    function useInterval(callback, delay) {
      const savedCallback = useRef();
    
      // Remember the latest callback.
      useEffect(() => {
        savedCallback.current = callback;
      }, [callback]);
    
      // Set up the interval.
      useEffect(() => {
        function tick() {
          savedCallback.current();
        }
        if (delay !== null) {
          let id = setInterval(tick, delay);
          return () => clearInterval(id);
        }
      }, [delay]);
    }
    

    Usage

      useInterval(() => {
        setTimeLeft((prev) => (prev > 0 ? prev - 1 : 0));
      }, delay);
    
    Login or Signup to reply.
  3. It would be easier to create a hook called useTimer to handle counting down the time. That way, each <Timer> only needs to know the time it will render.

    const { useCallback, useEffect, useState } = React;
    
    const useTimer = ({ seconds, refreshRate = 50 }) => {
      const [timeLeft, setTimeLeft] = useState(seconds);
      const [intervalId, setIntervalId] = useState(null);
    
      useEffect(() => {
        const endTime = Date.now() + (seconds * 1e3);
        setIntervalId((currentIntervalId) => {
          if (currentIntervalId) {
            clearInterval(currentIntervalId);
          }
          const internalIntervalId = setInterval(() => {
            const diff = Math.floor((endTime - Date.now()) / 1e3);
            setTimeLeft(diff);
            if (diff < 1) {
              clearInterval(internalIntervalId);
            }
          }, refreshRate)
          return internalIntervalId;
        });
      }, [seconds, refreshRate]);
      
      const cancel = useCallback(() => {
        clearInterval(intervalId);
      }, [intervalId]);
      
      return { timeLeft, cancel };
    }
    
    const Timer = ({ seconds }) => {
      const { timeLeft, cancel } = useTimer({ seconds });
      
      // Cancel all timers after 30 seconds...
      useEffect(() => {
        if (cancel) {
          setTimeout(() => {
            cancel();
          }, 3e4); /* 30 seconds */
        }
      }, [cancel]);
    
      return (
        <div>
          {Math.floor(timeLeft / 60)}:
          {(timeLeft % 60).toString().padStart(2, '0')} 
        </div>
      );
    };
    
    const App = () => {
      return (
        <div>
          <Timer seconds={120} />
          <Timer seconds={60} />
          <Timer seconds={30} />
          <Timer seconds={10} />
        </div>
      );
    };
    
    ReactDOM
      .createRoot(document.getElementById("root"))
      .render(<App />);
    <div id="root"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search