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.

Revisions

  1. lucasmotta created this gist Sep 11, 2025.
    198 changes: 198 additions & 0 deletions useCountdown.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,198 @@
    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,
    };
    };