Muhammad Naveed Nawab
Muhammad Naveed Nawab

Reputation: 35

How to prevent the timer on reload the page in React js?

I am trying to prevent the counter from reloading on page refresh. Here is the code:

import React, { useState, useEffect } from "react";
import { useDispatch } from "react-redux";
import { timerEnd } from "./states-manager/timer-slice";

export default function Timer() {
  // const lol =localStorage.getItem('timer')

  const [countdown, setCountdown] = useState({
    expirityTime: 100,
    expired: false
  });

  const [timer, setTimer] = useState()
  const [focus, setFocus] = useState(true)
  const dispatch = useDispatch();
 
  // Stop Counter when tab is switched
  
useEffect(()=>{
  window.onblur = function () { 
    setFocus(false)
  }; 
  window.onfocus = function () { 
    setFocus(true)
    setTimer(countdown.expirityTime)

  }; 

})

  useEffect(() => {
    let time = countdown.expirityTime;
    if (time > 0 && (focus)) {
      const timerId = setTimeout(() => {
          setCountdown({ expirityTime:time- 1 }); 
      }, 1000);
      return () => clearTimeout(timerId);
    }
  });

  //Passing CountDown Vlaue to the Quiz.js through Redux Toolkit
  dispatch(timerEnd(timer));

  return (
    <div className="App">
      <div className="timer">
        Time Remaining: {Math.floor(countdown.expirityTime / 60)}:
        {countdown.expirityTime % 60}
      </div>
    </div>
  );
}

I am able to store the countdown time in local storage but it also goes to restart due to the code. Let me know if you can help me with this. If you need any further information, please let me know.

Upvotes: 0

Views: 2874

Answers (2)

PeterT
PeterT

Reputation: 517

I made a react hook which can be used as a timer or a count down timer at client side, please find the code and exmaple at github: PersistantTimer

No dependency required except react.

or you can find the code below:

import { useState, useEffect, useRef } from "react";

const initTimer = { //initial value of ref
    lastSavedElapsedTime: 0,
    elapsedTime:  0,
    intervalId: null as ReturnType<typeof setInterval> | null,
    start: 0,
    manuallyPaused: false
}
interface OPTIONS {
    updateFrequency?: number,
    maximumValue?:number,
    callback?:(()=>void)
    LocalStorageItemName?:string
}
const defaultOptions:OPTIONS = {

    updateFrequency: 1,
    maximumValue:0,
    callback:undefined,
    LocalStorageItemName:'Persistant_timer'
}
const usePersistantTimer = (
    pauseOnNoFocus: boolean = true, {
    updateFrequency = 1,
    maximumValue = 0,
    callback,
    LocalStorageItemName= 'Persistant_timer'}:OPTIONS = defaultOptions
): [number, () => void, () => void, () => void] => {
    const timer = useRef(initTimer)
    const cu = timer.current
    const getValueFromLocalStorage = () => {
        let v = parseInt(localStorage.getItem(LocalStorageItemName) || '0')
        if (isNaN(v) || v < 0) v = 0
        cu.lastSavedElapsedTime = v
        cu.elapsedTime = 0
        cu.start = new Date().getTime()
    }
    const [elapsedTime, setElapsedTime] = useState(cu.lastSavedElapsedTime)

    const PIN = (i: number) => { // set parameter to default 1 if the paramenter is not legal number.
        return (i > 1 && Number.isInteger(i)) ? i : 1
    }
    const updateFrequnce = PIN(updateFrequency)
    const start = () => {
        if (cu.manuallyPaused) cu.manuallyPaused = false
        if (!cu.intervalId) {
            getValueFromLocalStorage()
            cu.intervalId = setInterval(() => {
                cu.elapsedTime = new Date().getTime() - cu.start + cu.lastSavedElapsedTime
                //t preserve real elapsed time. 

                if (!(cu.elapsedTime % updateFrequnce)) setElapsedTime(cu.elapsedTime)

                if (maximumValue && cu.elapsedTime >= maximumValue * 1000) {
                    if (callback) callback()
                    cu.elapsedTime = 0
                    cu.manuallyPaused = true
                    pause()
                }
                localStorage.setItem(LocalStorageItemName, cu.elapsedTime.toString())
            }, 1000)
        }
    }
    const pause = () => {
        cu.lastSavedElapsedTime = cu.elapsedTime
        if (cu.intervalId) {
            clearInterval(cu.intervalId)
            cu.intervalId = null
        }
    }
    const manuallyPause = () => {
        cu.manuallyPaused = true
        pause()
    }
    const resetTimer = () => {
        cu.lastSavedElapsedTime = 0
        cu.elapsedTime = 0
        localStorage.setItem(LocalStorageItemName, "0")
        cu.start = new Date().getTime()
        setElapsedTime(0)
    }

    useEffect(() => {
        getValueFromLocalStorage()
        window.onblur = () => {
            if (pauseOnNoFocus) pause()
        }

        window.onfocus = () => {
            if (cu.manuallyPaused) return
            if (pauseOnNoFocus) start()
        }

        start()

        return () => {
            if (cu.intervalId) {
                clearInterval(cu.intervalId)
                cu.intervalId = null
            }
        }
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [pauseOnNoFocus, LocalStorageItemName, maximumValue, callback, updateFrequency])

    return [elapsedTime, start, manuallyPause, resetTimer]
}

export default usePersistantTimer

Upvotes: 1

Arky Asmal
Arky Asmal

Reputation: 1331

I would actually re-think how the current timer is implemented, to make it more accurate. But you can take advantage of the window before unload event, and set a parallel timer that updates local storage at an interval. That said you need to be careful with setting to local storage too frequently because it hits performance a good deal, if your saving a ton of objects/data, especially on smaller devices. This is because reading and writing to localstorage is a synchronous operation, and usually requires JSON.stringify and JSON.parse to write and read to it. My implementation of a timer is below. The key takeaways are that you need to:

  1. Update localstorage with timer data when the timer is paused, and unpaused
  2. Use a start time and end time to keep track of the timer, since a counter is inaccurate (React state updates are not immediate all the time)
  3. Run a function at an interval that updates local storage. The lower the interval, the more expensive it is in terms of performance.
import React, { useState, useEffect } from "react";
import { unstable_batchedUpdates } from "react-dom";
import {useDispatch} from 'react-redux'
function addMinutes(date, minutes) {
  return new Date(date.getTime() + minutes * 60000);
}
function addMillseconds(date, millseconds) {
  return new Date(date.getTime() + millseconds);
}
export default function Timer() {
  const currDate = new Date();
  const [timeInactiveStart, setTimeInactiveStart] = useState(currDate);
  const [inactiveTimerActive, setInactiveTimerActive] = useState(false);
  const [timeStart, setTimeStart] = useState(currDate);
  const [timeEnd, setTimeEnd] = useState(addMinutes(currDate, 10));
  const timeRemaining = timeEnd.getTime() - timeStart.getTime();
  const dispatch = useDispatch()
  // Stop Counter when tab is switched
  useEffect(() => {
    window.onblur = function () {
      //start parallel timer
      const timeStartDate = new Date();
      //we save here in case the user closes the page while away.
      //that way we still know how much time they have left
      const timerObj = {
        startTime: timeStartDate,
        endTime: timeEnd,
        remainingTime: timeEnd.getTime() - timeStartDate.getTime(),
        inactiveTimerActive: true,
      };
      localStorage.setItem("timerData".JSON.stringify(timerObj));
      setTimeInactiveStart(timeStartDate);
      //stop timer
      setInactiveTimerActive(true);
    };
    window.onfocus = function () {
      //end parallel timer
      const timeInactiveEnd = new Date();
      const timeElapsedInactive =
        timeInactiveEnd.getTime() - timeInactiveStart.getTime();
      //add time to intervals and now we can store them, so its not out of sync
      const newEndTime = addMillseconds(timeEnd, timeElapsedInactive);
      const newStartTime = addMillseconds(timeStart, timeElapsedInactive);
      const timerObj = {
        startTime: newStartTime.toString(),
        endTime: newEndTime.toString(),
        //we store this in case a user exists the page, we have a restarting point
        remainingTime: newEndTime.getTime() - newStartTime.getTime(),
        inactiveTimerActive: false,
      };
      unstable_batchedUpdates(() => {
        localStorage.setItem("timerData", JSON.stringify(timerObj));
        setTimeEnd(newEndTime);
        setTimeStart(newStartTime);
        //restart timer
        setInactiveTimerActive(false);
      });
    };
    window.onbeforeunload = function () {
      //by nature this wont always occur, so
      //if you need to keep the timer countdown with higher integrity,
      // consider updating to local storage every minute or 5 minutes, depending on
      // your use case. However, every second would be too frequent
      const timerObj = {
        startTime: timeStart.toString(),
        endTime: timeEnd.toString(),
        //we store this in case a user exists the page, we have a restarting point
        remainingTime: timeEnd.getTime() - timeStart.getTime(),
        inactiveTimerActive: inactiveTimerActive,
      };
      localStorage.setItem("timerData", JSON.stringify(timerObj));
    };
  });
  //set a timer for a custom interval.
   //  To create checkpoints. However, the lower the time between intervals,
  //the larger the performance hit, so don't update every second. In this example it updates every minute
  useEffect(() => {
    const updateStorage = setInterval(() => {
      const timerObj = {
        startTime: timeStart,
        endTime: timeEnd,
        inactiveTimerActive: inactiveTimerActive,
        remainingTime: timeEnd.getTime() - timeStart.getTime(),
      };
      localStorage.setItem("timerData", JSON.stringify(timerObj));
    }, 60000);
    return () => clearInterval(updateStorage);
  }, [timeEnd, timeStart, inactiveTimerActive]);

  useEffect(() => {
    const timer = setInterval(() => {
      //we increment if these are correct
      if (!inactiveTimerActive && timeStart.getTime() < timeEnd.getTime()) {
        setTimeStart(new Date());
      }
      //clear local storage if timer has ended
      else if (timeStart.getTime() > timeEnd.getTime())
        localStorage.removeItem("timerData");
    }, 1000);
    return () => clearInterval(timer);
  }, [inactiveTimerActive, timeStart, timeEnd]);

  //on mount we fetch from localstorage our timer values
  useEffect(() => {
    const timerData = localStorage.getItem("timerData");
    if (timerData) {
      const data = JSON.parse(timerData);
      const newDate = new Date();
      unstable_batchedUpdates(() => {
        setTimeStart(new Date());
        //add time remaining since timer was inactive when tab was closed
        setTimeEnd(addMillseconds(newDate, data.remainingTime));
      });
    }
  }, []);
  //here you can pass time remaining anywhere, etc.
  //Passing CountDown Vlaue to the Quiz.js through Redux Toolkit
  //dispatch(timerEnd(timer));
  return (
    <div className="App">
      <div className="timer">
        Time Remaining:{" "}
        {timeRemaining > 0 ? Math.floor(timeRemaining / 1000) : 0} seconds
      </div>
    </div>
  );
}

Upvotes: 1

Related Questions