Skip Main Navigation
Ben IlegboduBen Ilegbodu

Copy to clipboard React custom Hook

A React custom Hook wrapping the Clipboard web API to enable a copy to clipboard UX

Sunday, March 21, 2021 · 3 min read

The thing I like about React custom Hooks is that we can create a Hook for nearly anything. On the blog index, each blog post card has a "Copy URL" button that copies the URL of the post to the clipboard. Back in the day, this was only possible on the web with Flash (RIP). But now thanks to the Clipboard API we can do this directly in the browser. No Flash needed.

The Clipboard interface has a writeText() method for writing text to the system clipboard. On its own it's fairly straightforward. But we can wrap it in a React custom hook to also manage the success/failure states of the copy. Here's the full code:

const useCopyToClipboard = (text, notifyTimeout = 2500) => {
  const [copyStatus, setCopyStatus] = useState('inactive')
  const copy = useCallback(() => {
    navigator.clipboard.writeText(text).then(
      () => setCopyStatus('copied'),
      () => setCopyStatus('failed'),
    )
  }, [text])

  useEffect(() => {
    if (copyStatus === 'inactive') {
      return
    }

    const timeoutId = setTimeout(() => setCopyStatus('inactive'), notifyTimeout)

    return () => clearTimeout(timeoutId)
  }, [copyStatus])

  return [copyStatus, copy]
}

const CopyUrlButton = ({ url }) => {
  const [copyUrlStatus, copyUrl] = useCopyToClipboard(url)
  const buttonText = 'Copy URL'

  if (copyUrlStatus === 'copied') {
    buttonText = 'Copied'
  } else if (copyUrlStatus === 'failed') {
    buttonText = 'Copy failed!'
  }

  return <button onClick={copyUrl}>{buttonText}</button>
}

You can take this implementation of useCopyToClipboard and use it in your React app right away. It assumes one use per text to be copied. Review the Clipboard browser compatibility table to ensure it works in your supported browsers. But if you're interested in learning how all the parts work together, feel free to read on!

const [copyStatus, setCopyStatus] = useState('inactive')

We use the useState Hook to maintain the copy status. It's one of 'inactive' (the default state), 'success' (after a successful write to the clipboard) and 'failed' (after a failed write to the clipboard).

const copy = useCallback(() => {
  navigator.clipboard.writeText(text).then(
    () => setCopyStatus('copied'),
    () => setCopyStatus('failed'),
  )
}, [text])

We create a copy function that we'll return (along with the copyStatus) from our custom Hook. The UI calls this function when the user wants to copy the text (usually a button click). When the user copies the text, we call clipboard.writeText(), and change the copyStatus to 'success' or 'failed' depending on the outcome.

Our useCopyToClipboard Hook is re-executed every time that the component re-renders. We don't want to create a new function reference each time. Functions returned by custom Hooks often are passed as props to child components. If these functions are recreated with each re-render, they could cause unnecessary re-renders of the child components. As a result, it's typically good practice to ensure that functions returned by custom Hooks have stable references. And we create stable references by memoizing the functions using the useCallback Hook.

useEffect(() => {
  if (copyStatus === 'inactive') {
    return
  }

  const timeoutId = setTimeout(() => setCopyStatus('inactive'), notifyTimeout)

  return () => clearTimeout(timeoutId)
}, [copyStatus])

The user may want to copy again, so we need a way to reset the copyStatus. The useEffect Hook here sets a timeout for how long our custom Hook remains in the its 'success' or 'failed' state before automatically returning to the default 'inactive' state.

return [copyStatus, copy]

Lastly, our custom Hook returns the copyStatus and the memoized copy function as a 2-element "tuple" array for the component to use.

const CopyUrlButton = ({ url }) => {
  const [copyUrlStatus, copyUrl] = useCopyToClipboard(url)
  const buttonText = 'Copy URL'

  if (copyUrlStatus === 'copied') {
    buttonText = 'Copied'
  } else if (copyUrlStatus === 'failed') {
    buttonText = 'Copy failed!'
  }

  return <button onClick={copyUrl}>{buttonText}</button>
}

Within a component, we pass the text we want copied as a parameter to useCopyToClipboard. If the app provides UI for copying different pieces of text, we need multiple calls to useCopyToClipboard. Each one has its own copyStatus and copy function.

The component can create whatever UI it likes based on the copyStatus. In this example, the CopyUrlButton component uses the copyUrlStatus to control the button text. After the text is copied, the button text is 'Copied' or 'Copy failed', depending on the success or failure of the clipboard write. But after the timeout, the text returns to 'Copy URL'. Clicking the button, triggers its onClick prop, which is the copyUrl function.

That's it!

TypeScript for fun

For those using TypeScript the additional types needed are minimal.

type CopyStatus = 'inactive' | 'copied' | 'failed'

const useCopyToClipboard = (
  text: string,
  notifyTimeout = 2500,
): [CopyStatus, () => void] => {
  const [copyStatus, setCopyStatus] = useState<CopyStatus>('inactive')
  const copy = useCallback(() => {
    navigator.clipboard.writeText(text).then(
      () => setCopyStatus('copied'),
      () => setCopyStatus('failed'),
    )
  }, [text])

  useEffect(() => {
    if (copyStatus === 'inactive') {
      return
    }

    const timeoutId = setTimeout(() => setCopyStatus('inactive'), notifyTimeout)

    return () => clearTimeout(timeoutId)
  }, [copyStatus])

  return [copyStatus, copy]
}

Keep learning my friends. 🤓

Subscribe to the Newsletter

Get notified about new blog posts, minishops & other goodies


Hi, I'm Ben Ilegbodu. 👋🏾

I'm a Christian, husband, and father of 3, with 15+ years of professional experience developing user interfaces for the Web. I'm a Google Developer Expert Frontend Architect at Stitch Fix, and frontend development teacher. I love helping developers level up their frontend skills.

Discuss on Twitter // Edit on GitHub