useTimeout
A React hook for managing setTimeout with automatic cleanup and manual controls. Supports resettable timers, delayed actions, and null-delay pausing — all stale-closure safe.
Installation
npx shadcn@latest add https://r.fiberui.com/r/hooks/use-timeout.jsonA declarative wrapper around setTimeout that provides clear and reset controls. The timeout is automatically cleaned up on unmount, and the callback always has access to the freshest state (no stale closures).
Source Code
View the full hook implementation in the Hook Source Code section below.
Features
- Auto-Cleanup — Clears the timeout when the component unmounts
- Resettable — Call
reset()to restart the timer from scratch - Pausable — Pass
nullas the delay to prevent execution - Stale Closure Safe — The callback ref is updated every render, so it always sees the latest state
- Zero Dependencies — Uses only React's built-in hooks
Learn More
MDN: setTimeout()
The Web API that powers this hook
React: useEffect Cleanup
Why cleanup matters for timers
Auto-Hide Notification
A notification that automatically dismisses itself after 3 seconds. Click the reset button to keep it visible, or dismiss it manually.
Click the button to show a self-dismissing notification
"use client";
import { useTimeout } from "@repo/hooks/utility/use-timeout";
import { Button } from "@repo/ui/components/button";
import { useState } from "react";
import { Bell, X, RotateCcw } from "lucide-react";
export const Example1 = () => {
const [visible, setVisible] = useState(false);
// Auto-hide the notification after 3 seconds
const { reset, clear } = useTimeout(
() => {
setVisible(false);
},
visible ? 3000 : null,
);
const show = () => {
setVisible(true);
reset();
};
const dismiss = () => {
clear();
setVisible(false);
};
return (
<div className="flex flex-col items-center gap-4 p-6">
<Button variant="outline" onPress={show}>
<Bell className="mr-2 h-4 w-4" />
Show Notification
</Button>
{visible && (
<div className="flex w-full max-w-sm items-center gap-3 rounded-lg border bg-zinc-950 p-4 text-white shadow-lg">
<Bell className="h-5 w-5 shrink-0 text-blue-400" />
<div className="flex-1">
<p className="text-sm font-medium">New message!</p>
<p className="text-xs text-zinc-400">
This will auto-hide in 3 seconds.
</p>
</div>
<div className="flex gap-1">
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-zinc-400 hover:text-white"
onPress={() => reset()}
aria-label="Reset timer"
>
<RotateCcw className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-zinc-400 hover:text-white"
onPress={dismiss}
aria-label="Dismiss"
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
</div>
)}
<p className="text-muted-foreground text-xs">
{visible
? "Notification visible — resets on click, auto-hides after 3s"
: "Click the button to show a self-dismissing notification"}
</p>
</div>
);
};
Debounced Search
Uses useTimeout to delay the search until the user stops typing for 500ms. Each keystroke resets the timer, effectively debouncing the input.
How it works
Every keystroke calls reset(), which clears the previous timeout and
starts a new one. The search only fires when the user pauses for 500ms.
Search is debounced — waits 500ms after you stop typing.
"use client";
import { useTimeout } from "@repo/hooks/utility/use-timeout";
import { useState, useCallback } from "react";
import { Search, Loader2 } from "lucide-react";
const MOCK_RESULTS = [
"React Hooks Guide",
"React Server Components",
"React Performance Tips",
"React Testing Library",
"React Design Patterns",
];
export const Example2 = () => {
const [query, setQuery] = useState("");
const [results, setResults] = useState<string[]>([]);
const [isSearching, setIsSearching] = useState(false);
// Debounce the search — only fire 500ms after the user stops typing
const { reset } = useTimeout(
() => {
if (query.trim()) {
setIsSearching(true);
// Simulate API delay
setTimeout(() => {
setResults(
MOCK_RESULTS.filter((r) =>
r.toLowerCase().includes(query.toLowerCase()),
),
);
setIsSearching(false);
}, 300);
} else {
setResults([]);
}
},
query ? 500 : null,
);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
setIsSearching(false);
setResults([]);
reset();
},
[reset],
);
return (
<div className="mx-auto w-full max-w-sm space-y-3 p-6">
<div className="relative">
<Search className="text-muted-foreground absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" />
<input
type="text"
value={query}
onChange={handleChange}
placeholder="Search articles..."
className="bg-background h-10 w-full rounded-lg border pl-9 pr-4 text-sm outline-none focus:ring-2 focus:ring-blue-500"
/>
{isSearching && (
<Loader2 className="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 animate-spin text-blue-500" />
)}
</div>
{results.length > 0 && (
<ul className="space-y-1 rounded-lg border p-2">
{results.map((result) => (
<li
key={result}
className="text-muted-foreground hover:bg-muted cursor-pointer rounded-md px-3 py-2 text-sm transition-colors"
>
{result}
</li>
))}
</ul>
)}
{query && !isSearching && results.length === 0 && (
<p className="text-muted-foreground text-center text-xs">
No results found for "{query}"
</p>
)}
<p className="text-muted-foreground text-center text-xs">
Search is debounced — waits 500ms after you stop typing.
</p>
</div>
);
};
Idle Timeout Warning
Simulates a session timeout: after 5 seconds of inactivity a warning appears, and after 3 more seconds the session expires. Any interaction resets the timer chain.
Your session is active. Stop interacting to trigger the idle warning.
Idle → 5s → Warning → 3s → Expired
"use client";
import { useTimeout } from "@repo/hooks/utility/use-timeout";
import { Button } from "@repo/ui/components/button";
import { useState, useCallback } from "react";
import { ShieldAlert, MousePointerClick } from "lucide-react";
export const Example3 = () => {
const [status, setStatus] = useState<"active" | "warning" | "expired">(
"active",
);
// After 5 seconds of inactivity, show warning
const { reset: resetWarning } = useTimeout(
() => {
setStatus("warning");
},
status === "active" ? 5000 : null,
);
// After 8 seconds total (3s after warning), expire
const { reset: resetExpiry } = useTimeout(
() => {
setStatus("expired");
},
status === "warning" ? 3000 : null,
);
const handleActivity = useCallback(() => {
setStatus("active");
resetWarning();
resetExpiry();
}, [resetWarning, resetExpiry]);
const statusConfig = {
active: {
color: "bg-green-500",
border: "border-green-500/20",
label: "Session Active",
description:
"Your session is active. Stop interacting to trigger the idle warning.",
},
warning: {
color: "bg-yellow-500",
border: "border-yellow-500/20",
label: "Idle Warning",
description:
"You've been idle for 5 seconds. Session expires in 3 seconds...",
},
expired: {
color: "bg-red-500",
border: "border-red-500/20",
label: "Session Expired",
description: "Your session has timed out due to inactivity.",
},
};
const config = statusConfig[status];
return (
<div className="flex flex-col items-center gap-4 p-6">
<div
className={`w-full max-w-sm rounded-lg border ${config.border} p-6 transition-all duration-300`}
>
<div className="mb-4 flex items-center gap-3">
<ShieldAlert className="h-5 w-5" />
<div className="flex items-center gap-2">
<span
className={`h-2.5 w-2.5 rounded-full ${config.color} ${status === "warning" ? "animate-pulse" : ""}`}
/>
<span className="text-sm font-semibold">
{config.label}
</span>
</div>
</div>
<p className="text-muted-foreground mb-4 text-sm">
{config.description}
</p>
<Button
variant="outline"
className="w-full"
onPress={handleActivity}
>
<MousePointerClick className="mr-2 h-4 w-4" />
{status === "expired"
? "Restart Session"
: "I'm still here!"}
</Button>
</div>
<p className="text-muted-foreground text-xs">
Idle → 5s → Warning → 3s → Expired
</p>
</div>
);
};
Common Patterns
Delayed Action (one-shot)
const { clear } = useTimeout(() => {
setShowModal(false);
}, 5000);Conditional Execution
// Only schedule when isReady is true
const { reset } = useTimeout(() => doSomething(), isReady ? 2000 : null);Reset on Interaction
const { reset } = useTimeout(() => hideTooltip(), 3000);
return <div onMouseMove={reset}>Hover content</div>;API Reference
Hook Signature
function useTimeout(
callback: () => void,
delay: number | null,
): UseTimeoutReturn;Parameters
| Name | Type | Description |
|---|---|---|
callback | () => void | The function to execute after the delay. |
delay | number | null | Delay in milliseconds. Pass null to prevent execution. |
UseTimeoutReturn
| Name | Type | Description |
|---|---|---|
clear | () => void | Clears the active timeout. Safe to call multiple times. |
reset | () => void | Clears the current timeout and starts a new one immediately. |
Delay of 0
A delay of 0 is valid and pushes the callback to the next event-loop tick.
Only null prevents execution entirely.
Hook Source Code
import { useEffect, useRef, useCallback } from "react";
/**
* Return type for useTimeout hook.
*/
export interface UseTimeoutReturn {
/**
* Clears the active timeout safely.
*/
clear: () => void;
/**
* Resets the timeout (clears existing and starts a new one).
*/
reset: () => void;
}
/**
* A React hook for handling `setTimeout` with manual controls.
*
* - **Auto-Cleanup:** Clears timeout on unmount.
* - **Resettable:** Great for delaying actions (like hiding a tooltip).
* - **Stale Closure Safe:** Callback always has access to fresh state.
*
* @param callback - The function to execute after the delay.
* @param delay - The delay in milliseconds. Pass `null` to prevent execution.
* @returns An object containing `clear` and `reset` functions.
*
* @example
* ```tsx
* const { reset, clear } = useTimeout(() => {
* setShowModal(false);
* }, 5000);
*
* // Reset the timer if the user moves their mouse
* <div onMouseMove={reset}>
* Don't hide me yet!
* </div>
* ```
*/
export function useTimeout(
callback: () => void,
delay: number | null,
): UseTimeoutReturn {
const savedCallback = useRef(callback);
const timeoutId = useRef<ReturnType<typeof setTimeout> | null>(null);
// 1. Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// 2. Define control functions (memoized to be stable dependencies)
const clear = useCallback(() => {
if (timeoutId.current) {
clearTimeout(timeoutId.current);
timeoutId.current = null;
}
}, []);
const set = useCallback(() => {
if (delay === null) return;
timeoutId.current = setTimeout(() => {
savedCallback.current();
}, delay);
}, [delay]);
const reset = useCallback(() => {
clear();
set();
}, [clear, set]);
// 3. Set up the timeout on mount or delay change
useEffect(() => {
set();
return clear;
}, [delay, set, clear]);
return { clear, reset };
}
export default useTimeout;
useThrottledState
A hook for throttling state updates to improve performance. Ensures that state changes only propagate at a controlled rate, preventing UI lag during rapid updates.
useBattery
A React hook for monitoring device battery status. Provides real-time updates on charging state, level, and charging/discharging times via the Battery Status API.