Skip to content

Instantly share code, notes, and snippets.

@lucasmotta
Created September 11, 2025 14:44
Show Gist options
  • Select an option

  • Save lucasmotta/9727b1e6d674d32400b5f9b86d049bc9 to your computer and use it in GitHub Desktop.

Select an option

Save lucasmotta/9727b1e6d674d32400b5f9b86d049bc9 to your computer and use it in GitHub Desktop.
Use countdown that saves timer to localStorage
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