useTimeout
EditSchedules the latest callback once with reset, cancel, and pending controls.
import { useCallback, useEffect, useLayoutEffect, useRef, useState,} from "react";
export type UseTimeoutReturn = { reset: () => void; cancel: () => void; isPending: boolean;};
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
/** Schedules the latest callback once; a null delay disables the timeout. */export const useTimeout = ( callback: (this: void) => void, delayMs: number | null,): UseTimeoutReturn => { if ( delayMs !== null && (!Number.isFinite(delayMs) || delayMs < 0) ) { throw new RangeError("delayMs must be null or a finite, non-negative number"); }
const callbackRef = useRef(callback); const timerRef = useRef<ReturnType<typeof setTimeout>>(); const mountedRef = useRef(false); const [isPending, setIsPending] = useState(delayMs !== null);
useIsomorphicLayoutEffect(() => { callbackRef.current = callback; }, [callback]);
const clearTimer = useCallback(() => { if (timerRef.current !== undefined) clearTimeout(timerRef.current); timerRef.current = undefined; }, []);
const cancel = useCallback(() => { clearTimer(); if (mountedRef.current) setIsPending(false); }, [clearTimer]);
const reset = useCallback(() => { clearTimer(); if (!mountedRef.current) return;
if (delayMs === null) { setIsPending(false); return; }
setIsPending(true); timerRef.current = setTimeout(() => { timerRef.current = undefined; if (!mountedRef.current) return; setIsPending(false); Reflect.apply(callbackRef.current, undefined, []); }, delayMs); }, [clearTimer, delayMs]);
useIsomorphicLayoutEffect(() => { mountedRef.current = true; reset();
return () => { mountedRef.current = false; clearTimer(); }; }, [clearTimer, reset]);
return { reset, cancel, isPending };};Download
Section titled “Download”wget -O src/hooks/useTimeout.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/react-hooks/useTimeout.tstemp_dir="$(mktemp -d)" && bunx degit jrTilak/lazykit/registry/react-hooks "$temp_dir" && mv "$temp_dir/useTimeout.ts" src/hooks/useTimeout.ts && rm -r "$temp_dir"Examples
Section titled “Examples”import { useState } from "react";
import { useTimeout } from "./useTimeout";
export const TemporaryNotice = () => { const [visible, setVisible] = useState(true); const timeout = useTimeout(() => setVisible(false), visible ? 3_000 : null);
return ( <section> {visible && <p>This notice closes after three seconds.</p>} <button type="button" onClick={() => { setVisible(true); timeout.reset(); }} > Show again </button> </section> );};callback(() => void) — Latest callback to invoke when the timeout completes.delayMs(number | null) — Finite, non-negative delay.nulldisables and clears the timeout;0is valid.
Returns
Section titled “Returns”An object containing:
reset()— Restarts the timeout using the current delay.cancel()— Clears the timeout.isPending(boolean) — Whether a timeout is scheduled.
Changing only the callback does not restart the timer. Changing the delay does. A caller’s layout effect can cancel the scheduled timeout before it fires.