Skip to content

useTimeout

Edit

Schedules the latest callback once with reset, cancel, and pending controls.

useTimeout.ts
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 };
};
Terminal
wget -O src/hooks/useTimeout.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/react-hooks/useTimeout.ts
useTimeout.example.tsx
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. null disables and clears the timeout; 0 is valid.

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.