useClipboard
EditCopies text through the Clipboard API and tracks the latest request safely.
import { useCallback, useEffect, useLayoutEffect, useRef, useState,} from "react";
export type ClipboardState = | { status: "idle"; text: undefined; error: undefined; } | { status: "success"; text: string; error: undefined; } | { status: "error"; text: undefined; error: unknown; };
export type UseClipboardOptions = { resetAfterMs?: number | null;};
export type UseClipboardReturn = { state: ClipboardState; isSupported: boolean; copy: (this: void, text: string) => Promise<void>; reset: () => void;};
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
const idleClipboardState = (): ClipboardState => ({ status: "idle", text: undefined, error: undefined,});
/** Copies text with the Clipboard API and tracks the latest request. */export const useClipboard = ( options: UseClipboardOptions = {},): UseClipboardReturn => { const resetAfterMs = options.resetAfterMs ?? 2_000; if ( resetAfterMs !== null && (!Number.isFinite(resetAfterMs) || resetAfterMs < 0) ) { throw new RangeError( "resetAfterMs must be null or a finite, non-negative number", ); }
const [state, setState] = useState<ClipboardState>(idleClipboardState); const [isSupported, setIsSupported] = useState(false); const mountedRef = useRef(false); const requestIdRef = useRef(0); const resetTimerRef = useRef<ReturnType<typeof setTimeout>>();
const clearResetTimer = useCallback(() => { if (resetTimerRef.current !== undefined) { clearTimeout(resetTimerRef.current); } resetTimerRef.current = undefined; }, []);
useIsomorphicLayoutEffect(() => { mountedRef.current = true; try { setIsSupported( typeof navigator !== "undefined" && typeof navigator.clipboard?.writeText === "function", ); } catch { setIsSupported(false); }
return () => { mountedRef.current = false; requestIdRef.current += 1; clearResetTimer(); }; }, [clearResetTimer]);
const reset = useCallback(() => { requestIdRef.current += 1; clearResetTimer(); if (mountedRef.current) setState(idleClipboardState); }, [clearResetTimer]);
const copy = useCallback( async (text: string): Promise<void> => { const requestId = requestIdRef.current + 1; requestIdRef.current = requestId; clearResetTimer();
try { if (typeof navigator === "undefined") { throw new Error("Clipboard API is not available"); }
const clipboard = navigator.clipboard; const writeText = clipboard?.writeText; if (typeof writeText !== "function") { throw new Error("Clipboard API is not available"); }
await Reflect.apply(writeText, clipboard, [text]);
if (mountedRef.current && requestIdRef.current === requestId) { setState({ status: "success", text, error: undefined });
if (resetAfterMs !== null) { resetTimerRef.current = setTimeout(() => { resetTimerRef.current = undefined; if ( mountedRef.current && requestIdRef.current === requestId ) { setState(idleClipboardState); } }, resetAfterMs); } } } catch (error) { if (mountedRef.current && requestIdRef.current === requestId) { setState({ status: "error", text: undefined, error }); } throw error; } }, [clearResetTimer, resetAfterMs], );
return { state, isSupported, copy, reset };};Download
Section titled “Download”wget -O src/hooks/useClipboard.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/react-hooks/useClipboard.tstemp_dir="$(mktemp -d)" && bunx degit jrTilak/lazykit/registry/react-hooks "$temp_dir" && mv "$temp_dir/useClipboard.ts" src/hooks/useClipboard.ts && rm -r "$temp_dir"Examples
Section titled “Examples”import { useClipboard } from "./useClipboard";
export const CopyButton = () => { const clipboard = useClipboard();
return ( <button type="button" disabled={!clipboard.isSupported} onClick={() => void clipboard.copy("hello@example.com")} > {clipboard.state.status === "success" ? "Copied" : "Copy email"} </button> );};options.resetAfterMs(number | null, default:2000) — Finite, non-negative delay before success returns to idle.nullkeeps the latest state.
Returns
Section titled “Returns”An object containing:
state— A discriminatedidle,success, orerrorstate with copiedtextorerror.isSupported(boolean) — Whethernavigator.clipboard.writeTextis available after mounting.copy(text)— ReturnsPromise<void>, updates only the latest request, and rethrows unavailable-API and write failures.reset()— Clears state and invalidates pending requests.
Unsupported Clipboard APIs report isSupported: false. Unmounted, reset, or superseded requests never update React state.