Last active
December 3, 2020 11:15
-
-
Save misterjohannesson/0f4e15987157d6301b817d77b6db128d to your computer and use it in GitHub Desktop.
useCountdown React Hook
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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