useClipboard
A reactive interface for the Clipboard API with automatic state resetting, SSR safety, and optimized timer management for a seamless "Copy to Clipboard" UX.
Introduction
The useClipboard hook provides a robust abstraction over the Browser Clipboard API. It manages the asynchronous nature of writing to the clipboard while handling the temporary "copied" state feedback, automatic cleanup of timers, and error reporting — all in a single, performant package.
Basic Usage
import { useClipboard } from '@/hooks/useClipboard';
function CopyButton({ text }) {
const { copy, copied } = useClipboard({ timeout: 2000 });
return (
<button onClick={() => copy(text)}>
{copied ? 'Copied!' : 'Copy Text'}
</button>
);
}API Reference
Parameters
Prop
Type
Returns
Prop
Type
Hook
import {useState, useRef, useCallback, useEffect} from 'react';
interface UseClipboardOptions {
timeout?: number;
onCopyError?: (error: Error) => void;
}
/**
* A hook to interact with the Navigator Clipboard API.
* * This hook manages the asynchronous nature of clipboard access,
* providing reactive state for success/error feedback and automatic
* cleanup of transition states.
*
* @param timeout - Time in ms to keep the `copied` state as true.
* @param onCopyError - Callback triggered when the clipboard write fails.
*/
export function useClipboard({
timeout = 1000,
onCopyError,
}: UseClipboardOptions = {}) {
const [error, setError] = useState<Error | null>(null);
const [copied, setCopied] = useState(false);
const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const onCopyErrorRef = useRef(onCopyError);
// Sync the error callback ref to avoid unnecessary hook invalidation
useEffect(() => {
onCopyErrorRef.current = onCopyError;
}, [onCopyError]);
const clearTimer = useCallback((): void => {
if (copyTimeoutRef.current !== null) {
clearTimeout(copyTimeoutRef.current);
copyTimeoutRef.current = null;
}
}, []);
const reset = useCallback((): void => {
setCopied(false);
setError(null);
clearTimer();
}, [clearTimer]);
// Clean up timer on unmount
useEffect(() => clearTimer, [clearTimer]);
const copy = useCallback(
async (valueToCopy: string): Promise<void> => {
// Clear previous states before starting a new operation
reset();
// Early return if API is unavailable (SSR or unsupported browser)
if (typeof navigator === 'undefined' || !navigator.clipboard) {
const errorInstance = new Error('Clipboard API is not available.');
setError(errorInstance);
onCopyErrorRef.current?.(errorInstance);
return;
}
try {
await navigator.clipboard.writeText(valueToCopy);
setCopied(true);
copyTimeoutRef.current = setTimeout(() => {
setCopied(false);
copyTimeoutRef.current = null;
}, timeout);
} catch (error_: unknown) {
const errorInstance =
error_ instanceof Error ? error_ : new Error(String(error_));
setError(errorInstance);
onCopyErrorRef.current?.(errorInstance);
}
},
[timeout, reset]
);
return {copy, reset, error, copied};
}Advanced Examples
Handling Sensitive Data
For passwords or API keys, we use a longer timeout for visibility, but provide a manual reset. This allows users to hide the "Copied" status immediately after they are done, which is a great security-conscious UX pattern.
import { useClipboard } from '@/hooks/useClipboard';
export function ApiKeyField({ apiKey }) {
// Longer timeout for visibility
const { copy, copied, error, reset } = useClipboard({ timeout: 8000 });
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
<input
type="password"
value={apiKey}
readOnly
className="p-2 border rounded"
/>
<button onClick={() => copy(apiKey)}>
{copied ? 'Success!' : 'Copy Key'}
</button>
{copied && (
<button onClick={reset} className="text-xs text-gray-500 underline">
Hide Status
</button>
)}
</div>
{error && <p className="text-sm text-red-600 font-medium">{error.message}</p>}
</div>
);
}Integrated Logging
Use the onCopyError callback to log failures to your telemetry service without adding logic to your UI components.
const { copy } = useClipboard({
onCopyError: (err) => {
myTelemetry.log('ClipboardFailure', { message: err.message });
}
});Why use this?
Deterministic State Transitions
By calling reset() at the start of every copy() execution, we ensure the hook transitions from a clean slate. This prevents "state ghosting" where an old error might persist momentarily during a new, successful attempt.
Ref-Based Callback Stability
We store the onCopyError callback in a useRef.
Architecture Note
This pattern allows the copy function to remain stable (no re-renders) even if the parent component provides a new anonymous function for onCopyError on every render. It breaks the dependency chain and optimizes performance.
Memory Leak Protection
The hook utilizes a combination of clearTimer and a useEffect cleanup. If the user initiates a copy action and then immediately navigates away from the page, the timer is cleared, preventing a state update on an unmounted component.
Error Handling
The Clipboard API requires a Secure Context (HTTPS) and User Interaction.
Security Restrictions
Browsers will block navigator.clipboard if it is not triggered by a direct user gesture (like a click) or if the site is served over insecure HTTP.
- Do: Trigger
copy()directly fromonClickhandlers. - Do: Provide visual feedback using the
errorstate if the user has blocked clipboard permissions. - Don't: Attempt to copy text inside a
useEffecton mount.
Last updated on