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:
- Update localstorage with timer data when the timer is paused, and unpaused
- 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)
- 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>
);
}