Created
September 11, 2025 14:44
-
-
Save lucasmotta/9727b1e6d674d32400b5f9b86d049bc9 to your computer and use it in GitHub Desktop.
Use countdown that saves timer to localStorage
This file contains hidden or 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 * as React from 'react'; | |
| export interface UseCountdownOptions { | |
| key: string; | |
| duration: number; | |
| autoStart?: boolean; | |
| onComplete?: () => void; | |
| } | |
| export const useCountdown = ({ | |
| key, | |
| duration, | |
| autoStart = false, | |
| onComplete, | |
| }: UseCountdownOptions) => { | |
| const [timeLeft, setTimeLeft] = React.useState<number>(0); | |
| const [completed, setCompleted] = React.useState<boolean>(false); | |
| const [running, setRunning] = React.useState<boolean>(false); | |
| const intervalRef = React.useRef<NodeJS.Timeout | null>(null); | |
| const storageKey = React.useMemo(() => `countdown_${key}`, [key]); | |
| const safeClearInterval = React.useCallback(() => { | |
| if (intervalRef.current) { | |
| clearInterval(intervalRef.current); | |
| intervalRef.current = null; | |
| } | |
| }, []); | |
| const saveToStorage = React.useCallback( | |
| (endTime: number, isActive: boolean, isCompleted = false) => { | |
| if (typeof window !== 'undefined') { | |
| localStorage.setItem( | |
| storageKey, | |
| JSON.stringify({ | |
| endTime, | |
| isActive, | |
| duration, | |
| completed: isCompleted, | |
| }), | |
| ); | |
| } | |
| }, | |
| [storageKey, duration], | |
| ); | |
| const loadFromStorage = React.useCallback(() => { | |
| if (typeof window === 'undefined') { | |
| return null; | |
| } | |
| try { | |
| const stored = localStorage.getItem(storageKey); | |
| if (!stored) { | |
| return null; | |
| } | |
| const data = JSON.parse(stored); | |
| if (data.duration !== duration) { | |
| localStorage.removeItem(storageKey); | |
| return null; | |
| } | |
| return data; | |
| } catch { | |
| localStorage.removeItem(storageKey); | |
| return null; | |
| } | |
| }, [storageKey, duration]); | |
| const startCountdown = React.useCallback( | |
| (fromTime?: number) => { | |
| safeClearInterval(); | |
| const now = Date.now(); | |
| const endTime = fromTime || now + duration * 1000; | |
| const remaining = Math.max(0, endTime - now); | |
| if (remaining <= 0) { | |
| setTimeLeft(0); | |
| setCompleted(true); | |
| setRunning(false); | |
| saveToStorage(endTime, false, true); | |
| onComplete?.(); | |
| return; | |
| } | |
| setTimeLeft(Math.ceil(remaining / 1000)); | |
| setCompleted(false); | |
| setRunning(true); | |
| saveToStorage(endTime, true); | |
| intervalRef.current = setInterval(() => { | |
| const currentRemaining = Math.max(0, endTime - Date.now()); | |
| const currentSeconds = Math.ceil(currentRemaining / 1000); | |
| setTimeLeft(currentSeconds); | |
| if (currentRemaining <= 0) { | |
| setCompleted(true); | |
| setRunning(false); | |
| safeClearInterval(); | |
| saveToStorage(endTime, false, true); | |
| onComplete?.(); | |
| } | |
| }, 1000); | |
| }, | |
| [safeClearInterval, duration, onComplete, saveToStorage, storageKey], | |
| ); | |
| const start = React.useCallback(() => { | |
| startCountdown(); | |
| }, [startCountdown]); | |
| const continueTimer = React.useCallback(() => { | |
| const stored = loadFromStorage(); | |
| if (stored && stored.isActive) { | |
| startCountdown(stored.endTime); | |
| } else { | |
| startCountdown(); | |
| } | |
| }, [loadFromStorage, startCountdown]); | |
| const reset = React.useCallback( | |
| (shouldStart = false) => { | |
| safeClearInterval(); | |
| setTimeLeft(duration); | |
| setCompleted(false); | |
| setRunning(false); | |
| localStorage.removeItem(storageKey); | |
| if (shouldStart) { | |
| startCountdown(); | |
| } | |
| }, | |
| [safeClearInterval, duration, startCountdown, storageKey], | |
| ); | |
| React.useEffect(() => { | |
| const stored = loadFromStorage(); | |
| if (stored) { | |
| if (stored.completed) { | |
| setTimeLeft(0); | |
| setCompleted(true); | |
| setRunning(false); | |
| return; | |
| } | |
| if (stored.isActive) { | |
| const now = Date.now(); | |
| const remaining = Math.max(0, stored.endTime - now); | |
| if (remaining > 0) { | |
| startCountdown(stored.endTime); | |
| } else { | |
| setTimeLeft(0); | |
| setCompleted(true); | |
| setRunning(false); | |
| saveToStorage(stored.endTime, false, true); | |
| onComplete?.(); | |
| } | |
| return; | |
| } | |
| } | |
| setTimeLeft(duration); | |
| if (autoStart) { | |
| startCountdown(); | |
| } | |
| }, [ | |
| autoStart, | |
| duration, | |
| loadFromStorage, | |
| onComplete, | |
| saveToStorage, | |
| startCountdown, | |
| storageKey, | |
| ]); | |
| React.useEffect( | |
| () => () => { | |
| safeClearInterval(); | |
| }, | |
| [safeClearInterval], | |
| ); | |
| return { | |
| timeLeft, | |
| completed, | |
| running, | |
| start, | |
| continue: continueTimer, | |
| reset, | |
| }; | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment