Skip to content

Instantly share code, notes, and snippets.

@misterjohannesson
Last active December 3, 2020 11:15
Show Gist options
  • Save misterjohannesson/0f4e15987157d6301b817d77b6db128d to your computer and use it in GitHub Desktop.
Save misterjohannesson/0f4e15987157d6301b817d77b6db128d to your computer and use it in GitHub Desktop.
useCountdown React Hook
import { parseExpression } from "cron-parser";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
const formatter = Intl.NumberFormat("en-US", {
minimumIntegerDigits: 2,
maximumFractionDigits: 0
});
type Hook<U> = (...x: any[]) => U;
type Return = {
countdown: number;
countdownParts: {
days: number;
hours: number;
mins: number;
secs: number;
};
countdownAsString: string;
isTriggered: boolean;
};
const CronRegex = /^((\d+|(\*(\/\d+)?)) ){1,7}(\d+|(\*(\/\d+)?))$/;
/**
* This hook provides a countdown in days to seconds towards the next iteration of a given cron.
* Once the countdown of an iteration has been reached the `isTriggered` return value will remain true based
* on the `triggerDuration` paramter is ms.
*
* Example usage:
* ```tsx
* const { countdownAsString } = useCountdown("*\/15 * * * *", 10000);
*
* return (
* <div>
* <h1>{countdownAsString}</h1>
* </div>
* );
* ```
*/
export const useCountdown: Hook<Return> = (cron: string, triggerDuration = 0, iteration = 1000) => {
/**
* Time remaining till next iteration in ms.
*/
const [timeLeft, setTimeLeft] = useState(0);
const [isTriggered, setIsTriggered] = useState(false);
/**
* Number parts of the countdown time remaining.
*/
const countdownParts = useMemo(() => {
const days = Math.floor(timeLeft / 864e5);
const hours = Math.floor((timeLeft % 864e5) / 36e5);
const mins = Math.floor((timeLeft % 36e5) / 6e4);
const secs = Math.floor((timeLeft % 6e4) / 1e3);
return {
days,
hours,
mins,
secs
};
}, [timeLeft]);
/**
* Formatted string in the dd:hh:mm:ss format
*/
const countdownAsString = useMemo(
() =>
`${formatter.format(countdownParts.days)}:${formatter.format(
countdownParts.hours
)}:${formatter.format(countdownParts.mins)}:${formatter.format(countdownParts.secs)}`,
[countdownParts]
);
const timerRef = useRef<NodeJS.Timeout>(null);
const triggerRef = useRef<boolean>(false);
const triggerTimeRef = useRef<number>(0);
const progressTimer = useCallback(() => {
const now = Date.now();
const previousIteration = parseExpression(cron).next().toDate().getTime();
const timeLeft = previousIteration - now;
setTimeLeft(timeLeft);
if (timeLeft < iteration && !triggerRef.current) {
triggerRef.current = true;
triggerTimeRef.current = now;
setIsTriggered(true);
} else if (triggerRef.current && now - triggerTimeRef.current > triggerDuration) {
triggerRef.current = false;
setIsTriggered(false);
}
timerRef.current = setTimeout(progressTimer, iteration);
}, [cron, triggerDuration, iteration]);
useEffect(() => {
if (CronRegex.test(cron)) timerRef.current = setTimeout(progressTimer, iteration);
return () => {
clearTimeout(timerRef.current);
};
}, [cron, triggerDuration, iteration]);
return {
countdown: timeLeft,
countdownParts,
isTriggered,
countdownAsString
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment