Hookipedia

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

useClipboard.ts
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 from onClick handlers.
  • Do: Provide visual feedback using the error state if the user has blocked clipboard permissions.
  • Don't: Attempt to copy text inside a useEffect on mount.

Last updated on

On this page

Edit this page on GitHub