Countdown hook loses 1 second every minute - javascript

I'm implementing a countdown in my react-native app, but something is not working properly.
It seems that the countdown loses 1 seconds every minute (as you can see in the gif, it jumps between 33 and 31)
this is the code:
import {
differenceInDays,
differenceInHours,
differenceInMinutes,
differenceInSeconds,
isBefore,
parseISO,
} from 'date-fns'
import { useEffect, useState } from 'react'
type CountdownResult = {
days: number
hours: number
minutes: number
seconds: number
}
const calculateInitialDuration = (endDate: string, today: Date): CountdownResult => {
const futureDate = new Date(endDate)
const days = differenceInDays(futureDate, today)
const hours = differenceInHours(futureDate, today) % 24
const minutes = differenceInMinutes(futureDate, today) % 60
const seconds = differenceInSeconds(futureDate, today) % 60
return { days, hours, minutes, seconds }
}
const EXPIREDRESULT: CountdownResult = { days: 0, hours: 0, minutes: 0, seconds: 0 }
// TODO: FIXME: sometimes the countdown jumps directly between 2 seconds
// even if the real time passed is 1 second
// this was happening before the refactor too
const useCountdown = (endDate: string): CountdownResult => {
const today = new Date()
const formattedEndDate = parseISO(endDate)
// doing this because at the beginning countdown seems stuck on the first second
// maybe there is a better solution for this problem
const initialCountdown = calculateInitialDuration(endDate, today)
initialCountdown.seconds++
const [time, setTime] = useState(isBefore(formattedEndDate, today) ? EXPIREDRESULT : initialCountdown)
useEffect(() => {
if (isBefore(formattedEndDate, today)) return
const intervalId = setInterval(() => {
setTime(calculateInitialDuration(endDate, today))
}, 1000)
return (): void => clearInterval(intervalId)
}, [time])
return time
}
export default useCountdown
endDate is a string following the ISO 8601 format.
I'm using date-fns but I also tried the basic javascript implementation, bug is still the same.
Another strange thing is that the countdown, at the beginning, is stuck for one second on the first second (that's the reason why I created the initialCountdown variable), but actually I don't like the solution.
Any tips? Where are the mistakes? Thanks in advance.

At the moment you are assuming that setInterval() triggers the callback every 1,000 milliseconds.
setInterval(() => {
setTime(calculateInitialDuration(endDate, today))
}, 1000)
Unfortunately, with everything else that browser has to do, there's no guarantee that it will.
What you will need to do to gain more accuracy is repeatedly use setTimeout() calculating how long to set the timeout for.
let timeout;
const start = (() => {
// IIFE because func needs to be able to reference itself!
let func = () => {
// Do whatever you need to do here
let now = new Date();
let timeToNextSecond = 1000 - (now.getTime() % 1000);
console.log('Now: ', now, 'TimeToNext: ', timeToNextSecond);
timeout = setTimeout(func, timeToNextSecond);
};
return func;
})();
const stop = () => clearTimeout(timeout);
start();
// wait 10 seconds(ish)
setTimeout(stop, 10000);
If you run this, you will see that subsequent timeouts run shortly after the start of the next second. Assuming that the browser isn't bogged down doing other stuff, it will run every second.
Thoughts: I imagine that setInterval does something like this behind the scenes, just with a fixed timeout causing the drift.

Related

Getting always initial value of state after updating the state from the local storage

I'm using react-timer-hook package in my next.js project to display a timer like you can see in the screenshot below:
Now the issue is I want to persist this timer elapsed time into the local storage and whenever the page reloads manually then I need the Timer to start from that specific elapsed time that I'm trying to get from the local storage, but whenever I reload the page manually then Timer starts from the initial state's value. below are the codes:
Timer Component
function Timer({ seconds, minutes, hours }) {
return (
<Typography variant="h5" fontWeight={'bold'} component={'div'}>
<span>{String(hours).padStart(2, '0')}</span>:
<span>{String(minutes).padStart(2, '0')}</span>:
<span>{String(seconds).padStart(2, '0')}</span>
</Typography>
);
}
I'm adding 3600 seconds into expiryTimestamp i.e., current date and time to get one hour of Timer.
let expiryTimestamp = new Date();
expiryTimestamp.setSeconds(expiryTimestamp.getSeconds() + 3600);
Aslo I'm using another state with same 3600 seconds initial value
const [elapsed, setElapsed] = useState(3600);
I'm using useEffect and decrementing the elapsed value on every second into local storage.
useEffect(() => {
const interval = setInterval(() => {
localStorage.setItem('elapsed', JSON.stringify(elapsed--));
}, 1000);
return () => clearInterval(interval);
}, [elapsed]);
Now I'm getting the elapsed value from the local storage
useEffect(() => {
const elapsed = localStorage.getItem('elapsed');
if (elapsed) {
setElapsed(JSON.parse(elapsed));
}
}, []);
Again I'm using another variable to create current date and time + elapsed value
let currentTime = new Date();
currentTime.setSeconds(currentTime.getSeconds() + elapsed);
Finally I'm passing the currentTime in useTimer hook
const { seconds, minutes, hours } = useTimer({
expiryTimestamp: currentTime,
onExpire: handleForm,
});
Elapsed time is properly storing in the local storage, but still Timer starts from 3600 seconds.
We can use expiryTimestamp value of use timer as function to initiate its value. Check the following component
import { useEffect } from 'react';
import { useTimer } from 'react-timer-hook';
export default function Timer() {
const { seconds, minutes, hours } = useTimer({
expiryTimestamp: () => {
/** determine the expiry time stamp value all at once */
const time = new Date(),
elapsedTime = Number(window.localStorage.getItem('elapsed')) || 3600;
time.setSeconds(time.getSeconds() + elapsedTime);
return time;
},
onExpire: () => alert('expired')
});
/** update elapsed value in local storage */
useEffect(() => {
const elapsedSeconds = hours * 60 * 60 + minutes * 60 + seconds;
window.localStorage.setItem('elapsed', elapsedSeconds);
}, [seconds]);
return (
<div>
<span>{String(hours).padStart(2, '0')}</span>:
<span>{String(minutes).padStart(2, '0')}</span>:
<span>{String(seconds).padStart(2, '0')}</span>
</div>
);
}
If you're using this component in next.js. You should use dynamic import with ssr disabled. otherwise you'll receive an error because SSR doesn't recognize the window.localStorage api. Check below
import dynamic from 'next/dynamic';
import React from 'react';
const Timer = dynamic(() => import('./timer'), { ssr: false });
export default function Index = () => {
return <Timer />;
};
It looks like your first useEffect hook is overwriting the value in local storage before you set your state from what's stored there.
Hooks fire in the order in which they are defined within a component, so your component is doing:
Set state to 3600
UseEffect where we set the local storage
UseEffect where we read local storage (whose value has just been overwritten to the default 3600) and set the state to that
You could try checking the local storage value and starting your interval in the same hook.
I'd first start by starting my default state with undefined
const [elapsed, setElapsed] = useState();
This lets me control the starting of the timer entirely within the useEffect
useEffect(() => {
// make sure your first action is to get the elapsed time in local storage
const storedElapsed = localStorage.getItem('elapsed');
// work out if this is the initial load or not
if (!elapsed) {
// component state is not yet set, so use either local storage or the default
const newElapsedState = JSON.parse(storedElapsed) || 3600;
setElapsed(newElapsedState);
} else {
// we now know that state is set correctly, so can ignore local storage and start the timer
const interval = setInterval(() => {
localStorage.setItem('elapsed', JSON.stringify(elapsed--));
}, 1000);
return () => clearInterval(interval);
}
}, [elapsed]);
You will need to handle your first render being undefined, but after that your flow should be consistent as you have control over what is being set in what order.
You can control this by the order in which you define your useEffects, but I personally find this way a bit clearer as the logic is grouped together.

Discord Bot Running Slow with Countdown Timer

I have a working countdown timer command with my discord bot and it works fairly well at the moment. The biggest issue that I am running into at this time is - if the command is executed again while there is already another instance running, it is extremely slow and will start to freeze up the command within discord.
I am expecting to be able to run this command more than once without any issue or lag. I am not sure what I can implement to make this command run faster when there is more than one instance of it running simultaneously.
const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js");
const moment = require("moment");
let eventMonth;
let eventDay;
let eventYear;
let time;
module.exports = {
data: new SlashCommandBuilder()
.setName("test-timer")
.setDescription("Timer testing command")
.setDefaultMemberPermissions(PermissionFlagsBits.ViewAuditLog)
.addIntegerOption((option) => option
.setName("event-month")
.setDescription("month of the event")
.setRequired(true)
)
.addIntegerOption((option) => option
.setName("event-day")
.setDescription("day of the event")
.setRequired(true)
)
.addIntegerOption((option) => option
.setName("event-year")
.setDescription("year of the event")
.setRequired(true)
)
.addStringOption((option) => option
.setName("event-time")
.setDescription("time of the event")
.setRequired(true)
),
async execute(interaction, client) {
const message = await interaction.reply({
content: `Days: 0 Hours: 0 - Mintues: 0 - Seconds: 0`,
fetchReply: true
});
eventMonth = interaction.options.getInteger("event-month").toString();
eventDay = interaction.options.getInteger("event-day").toString();
eventYear = interaction.options.getInteger("event-year").toString();
time = interaction.options.getString("event-time");
const convertTime12to24 = (time12h) => {
const [time, modifier] = time12h.split(' ');
let [hours, minutes] = time.split(':');
if (hours === '12') {
hours = '00';
}
if (modifier === 'PM') {
hours = parseInt(hours, 10) + 12;
}
return `${hours}:${minutes}`;
}
const timeFormat = `${convertTime12to24(time)}:00`
let interval;
const eventDayMoment = moment(`${eventYear}-${eventMonth}-${eventDay} ${timeFormat}`);
const second = 1000;
const minute = second * 60;
const hour = minute * 60;
const day = hour * 24;
const countDownFn = () => {
const today = moment();
const timeSpan = eventDayMoment.diff(today);
if (timeSpan <= -today) {
console.log("Past the event day");
clearInterval(interval);
return;
} else if (timeSpan <= 0) {
console.log("Today is the day of the event");
clearInterval(interval);
return;
} else {
const days = Math.floor(timeSpan / day);
const hours = Math.floor((timeSpan % day) / hour);
const minutes = Math.floor((timeSpan % hour) / minute);
const seconds = Math.floor((timeSpan % minute) / second);
message.edit({
content: `Days: ${days} Hours: ${hours} - Mintues: ${minutes} - Seconds: ${seconds}`
})
}
};
interval = setInterval(countDownFn, second);
}
};
Most APIs these days have ratelimits to keep people from spamming them and degrading service quality for everyone else. When you try to make requests after hitting the rate limit, your requests are blocked and you get a HTTP 429 Too Many Requests status code.
How Discord's Rate Limits work
Discord, specifically, has a global ratelimit of 50 requests per second for every bot. Some bots outgrow that limit, and have to contact their staff to get it increased.
On top of that, they also have per-resource rate limits, which are probaby the ones you're hitting here. This means that resources in different guilds or channels have different rate limits (and you'll probably be able to notice that your countdown timer doesn't slow down then ran in two different guilds at the same time).
And how Discord.js handles it
To help out a bit with this Rate Limiting madness, discord.js has a set of features that queue up your requests locally and only sends them to the Discord API whenever the Ratelimit allows it. This is why your countdown timer is slow when you have too many instances of it running simultaneously.
Instead of editing your message repeatedly you could use Unix timestamps (in seconds). [Docs]
Example:
<t:1672300000:R> => 17 hours ago
<t:1672400000:R> => in 11 hours
As a side effect of using the format the formatted and translated according to the user's preferences,
it shows the exact time and date when hovering over the timestamp
and automatically updates without spamming the discord api.

How to prevent a web timer to throttle on inactive tab(Chrome)?

I'm building simple web timer to calculate time working on tasks. Say, I have a 5 min timer and when time is up, I receive a notification.
I build it storing the time in state. But when the tab (Chrome) is inactive, because I'm actually working, the time throttle and gather some delay. (Chrome does that to conserve memory).
import React, { useEffect } from "react";
import { useFinishSession } from "../../hooks/useFinishSession";
const TimerIsActive = ({ setTimer, timerState, isActive }) => {
const { seconds, minutes, initialSeconds, initialMinutes } = timerState;
const { decreaseSeconds, decreaseMinutes, setSessionMinutes, setSessionSeconds } = setTimer;
const { finishSession, loading, error } = useFinishSession();
useEffect(() => {
let interval: NodeJS.Timer;
// reduce seconds and minutes by 1
if (isActive) {
interval = setInterval(() => {
if (seconds > 0) {
decreaseSeconds();
}
// when timer is finised, restart
if (seconds === 0 && minutes === 0) {
finishSession();
setSessionMinutes(initialMinutes);
setSessionSeconds(initialSeconds);
clearInterval(interval);
}
if (seconds === 0) {
decreaseMinutes();
}
}, 1000);
return () => clearInterval(interval);
}
}, [isActive, seconds, minutes]);
return null;
};
export default TimerIsActive;
How I go around that?
Here'are the solutions I'm contemplating:
Instead of storing the time in state, I will store the "start time" using a new Date() and calculate the difference every second to get an accurate time. (difference in seconds) --> my main concern with this approach is that the time only "correct" when the tab regain "focus". Hence, the notification won't fire "in time" when the timer is over.
Using service worker to calculate the time in the background.
What is the best options?
Similar question:
Countdown timer 'delays' when tab is inactive?

Wait minimum time before continuing

I have this function that waits for is_user_dict to return true, then uploads some profile data to a database and moves on with the program. While that's happening I have an animation I would like to play for at least 2 seconds if the rest of the function takes less than that much time to complete.
Previously I handled this using setTimeout, but that prevents the function from actually doing anything during that waiting period, slowing the program down needlessly. What's the best way for me to make sure showLogoAnimation runs for at least 2 seconds, but not longer than needed?
I know I could measure the time between start_time and the time the function is ready and use a while loop until that much time had passed, but as is I know my code is already sloppy and that there's probably a better way to do what I'm doing?
async function handleTestStart() {
traceFunction()
character.style.backgroundImage = "url(character.png)"
left_button.style.visibility = 'visible'
right_button.style.visibility = 'visible'
showLogoAnimation()
let start_time = new Date().getTime()
while (!is_user_dict) {
let end_time = new Date().getTime()
let time = end_time - start_time
if (time > 10000) {
timeoutError()
}
}
let is_profile_ready = createProfile()
let start_time2 = new Date().getTime()
is_profile_ready.then((result) => {
let end_time2 = new Date().getTime()
let time2 = end_time2 - start_time2
if (time2 > 10000) {
timeoutError()
}
hideLogoAnimation()
condition_category_array.forEach(function (condition_category) {
user_dict['more_info'].push(condition_category)
})
category = user_dict['more_info'][0]
backup_user_dict = deepCopyObject(user_dict)
nextQuestion()
})
}

React counter getting out of sync when tab is not focused

I’m building with React a timer that multiple people will see at the same time. I’ve noticed that, once the timer has been opened by two persons on different tabs, if the user looking at the timer changes of tab and comes back, the timer gets out of sync (dragged a few seconds).
Inside the timer component I’m providing a duration prop and I have a secondsToCountdown internal state that gets updated every second with an interval inside the component. The interval is something like this (using hooks btw):
const [secondsToCountdown, setSecondsToCountdown] = useState(duration * 60);
const tick = () => {
setSecondsToCountdown(secondsToCountdown - 1);
};
useInterval(
() => {
tick();
},
running ? 1000 : null
);
I’m guessing that for some reason the interval stops or runs slowly when the component is out of view. Is there a workaround for this? Am I doing something wrong here? Is the only solution just to use something along the lines of the visibilitychange event?
I think you can use requestAnimationFrame and instead counting down from a number you can compare the current datetime with the target datetime
I made a sample sandbox here https://codesandbox.io/s/0v3xo8p4yp.
import React, { useState } from "react";
import ReactDOM from "react-dom";
const countDown = 20 * 60 * 1000; //20 minutes
const defaultTargetDate = new Date().getTime() + countDown;
const App = () => {
const [targetDate, setTargetDate] = useState(new Date(defaultTargetDate));
const [remainingSeconds, setRemainingSeconds] = useState(countDown / 1000);
const countItDown = () =>
requestAnimationFrame(() => {
const diff = Math.floor((targetDate - new Date().getTime()) / 1000);
setRemainingSeconds(diff);
if (diff > 0) {
countItDown();
}
});
countItDown();
return <div>{remainingSeconds} sec</div>;
};

Categories

Resources