05 April 2023
Hector Sosa

Building React Hooks - Copy to Clipboard

How many times have you seen out there in the wild the need to copy to the clipboard? Whether it's a piece of code (no shame if it's from StackOverflow 💁🏻‍♂️ cough cough), a URL you want to share 💅🏻, or a simple text you need to refer elsewhere 🤓.
The easiest way to reduce friction in having something copied to the clipboard is by enabling buttons, and we all know that "Copy to clipboard" buttons are literally everywhere. So today we'll design a React Hook that enables our application to interact with the user's clipboard. Why hooks? 🤷🏻‍♂️
React applications are built from components. Generally, components are built using Hooks, whether they are built-in or custom.
A quick refresher on what you need to know about React Hooks:
  • All your Hooks should be written in camelCase and starting with use, such as useState. If your linter is configured for React, it should help you with this.
  • Hooks can only be called at the top level (before any returns). They shouldn't be called inside loops, conditions, or nested functions.
  • Only Hooks and components can call other Hooks! Therefore, functions that don’t call Hooks don’t need to be Hooks. You don't need hooks for everything.

Why Custom React Hooks?

Integrating custom React hooks that can be incredibly useful in different use cases. Here are a few examples:
  • useLocalStorage: This hook provides an easy way to store and retrieve data in the browser's localStorage. This can be useful for persisting data across sessions or keeping track of user preferences.
  • useMedia: This hook allows you to query the media query string and return a boolean indicating whether it matches or not. This can be useful for implementing responsive design, hiding or showing elements based on screen size, or dynamically changing styles.
  • usePrevious: This hook allows you to keep track of the previous value of a state variable. This can be useful when you need to compare the current and previous values and trigger a side effect based on the difference.
  • useAsync: This hook allows you to easily manage asynchronous operations with error handling, loading and data states. This can be useful for making API calls or handling long-running operations.
  • useDebounce: This hook lets you debounce the execution of a function by a specified delay. This is useful when you have an input field that triggers a search query, but you don't want to make the API call on every keystroke.
There are many other custom hooks available that can help you simplify your code and make it more reusable. The best hooks to use will depend on your specific use case and requirements.

How to use Clipboard.writeText()

The Clipboard interface's writeText() property writes the specified text string to the system clipboard.
2  .writeText("Text to copy")
3  .then(()=> /* Clipboard successfully set */)
4  .catch((err) => /* Clipboard write failed */);
If we go ahead and integrate this directly into a button, it could look something like this:
1import React from "react";
2import { CopyIcon, ErrorIcon, SuccessIcon } from "@/components/icons";
4export const CopyButton = ({ 
5    textToCopy, 
6    children 
7  }: { 
8    textToCopy: string, 
9    children?: React.ReactNode 
10  }) => {
11  const [clipboardState, setClipboardState] = React.useState<boolean | Error>(false);
12  return (
13    <button
14      onClick={() => {
15        navigator.clipboard
16          .writeText(code)
17          .then(() => setClipboardState(true))
18          .catch((e) => setClipboardState(e));
19      }}
20    >
21      {children}
22      {clipboardState ? (
23        clipboardState instanceof Error ? <ErrorIcon /> : <SuccessIcon />
24      ) : (
25        <CopyIcon />
26      )}
27    </button>
28  );
We could call it a day, but we can do better. At a simple glance, here's what we would need:
  • ⛔️ Check for support on navigator.clipboard. Even if we abstract this logic into a handler function our component would start to get messy.
  • ⛔️ We aren't resetting our UI after the operation. Setting a timeout would come in handy but this would require even more code.
  • ⛔️ The more this functionality is required in our codebase, the harder it'll get to adopt and maintain.

Do you need a reliable partner in tech for your next project?

Creating a Custom Hook

Custom Hooks should abstract as much logic as possible from the component that interacts with build-in React Hooks. We should only expose exactly what the UI needs to call the operation and provide the right feedback to the user.
Here is what our custom hook will do:
  • ✅ navigator.clipboard support check before executing operations;
  • ✅ Handle automatic resets after interacting with the clipboard;
  • ✅ Keep a simple and straight-forward hook to consume anywhere in our codebase.
1import React from "react";
2type CopyState = "READY" | "SUCCESS" | Error;
4export const useClipboard = ({ delay = 2500 } = {}) => {
5  const [state, setState] = React.useState<CopyState>("READY");
6  const [copyTimeout, setCopyTimeout] =
7    React.useState<ReturnType<typeof setTimeout>>();
9  function handleCopyResult(result: CopyState) {
10    setState(result);
11    clearTimeout(copyTimeout);
12    setCopyTimeout(setTimeout(() => setState("READY"), delay));
13  }
15  function copy(valueToCopy: string) {
16    if ("clipboard" in navigator) {
17      navigator.clipboard
18        .writeText(valueToCopy)
19        .then(() => handleCopyResult("SUCCESS"))
20        .catch((error) => error instanceof Error && handleCopyResult(error));
21    } else {
22      handleCopyResult(
23        new Error("`useClipboard`: Navigation Clipboard is not supported")
24      );
25    }
26  }
28  return { copy, state };

Is there a one-size-fits-all design approach?

Following patterns/conventions helps your consumer to understand what your hook does and where state, Effects, and other React features might be “hidden”. However, React Hooks may return arbitrary values.
Notice we aren't returning a traditional tuple [value, setValue]. Although, depending on the use case, it may be favorable to follow this design. Additionally, we could expose other methods/values in our hook depending on our application's needs.
Here are other options you can choose to build in and provide to your consumers:
  • isClipboardSupported to indicate whether or not the component can use the clipboard;
  • copiedText to provide feedback on what's in the clipboard by using clipboard.readText();
  • reset() method to reset failed operations.
As-is, with this abstraction in place, this is how all components that require clipboard interaction would look like with clipboard.copy and clipboard.state.
1import React from "react";
2import { CopyIcon, ErrorIcon, SuccessIcon } from "@/components/icons";
3import { useClipboard } from "@/hooks";
5export const CopyButton = ({ 
6    textToCopy, 
7    children 
8  }: { 
9    textToCopy: string, 
10    children?: React.ReactNode 
11  }) => {
12  /* ⬇️ Methods are now available to copy and check the hook's state */
13  const clipboard = useClipboard();
14  return (
15    <button onClick={() => clipboard.copy(textToCopy)}>
16      {children}
17      /* ⬇️ Type autocompletion will make it easy to structure your UI */
18      {clipboard.state === "READY" && <CopyIcon />}
19      {clipboard.state === "SUCCESS" && <SuccessIcon />}
20      {clipboard.state === "ERROR" && <ErrorIcon />}
21    </button>
22  );

Bonus Hook - Set and Clear Timeouts

Yes, you don't need hooks for everything, but there is still an additional level of abstraction we could achieve here. Notice how useClipboard clears and calls a timeout every single time the button is used. Executing a function after a specified delay is also a very common use case in React applications. So let's take it a step further and design a bonus hook: useTimeout.
Let's jot down what would we want from a useTimeout hook:
  • ✅ Methods to call and cancel a given timeout;
  • ✅ Control between timeouts;
  • ✅ Avoid triggering unnecessary re-renders.
This is a perfect use case to write a hook that interacts with useRef. Why? (a) Unlike state, refs are mutable and (b) Refs are not required for and don't trigger re-renders.
Normally, writing or reading ref.current during render is not allowed. However, it’s fine in the case below because the result is always the same, and the condition only executes during initialization so it’s fully predictable.
1import React from "react";
3export const useTimeout = (callback: Function, delay: number) => {
4  const callbackRef = React.useRef<Function | null>(null);
5  const timeoutIdRef = React.useRef<number | null>(null);
7  /* ⬇️ Only executed during initialization */
8  if (!callbackRef.current) {
9    callbackRef.current = callback;
10  }
12  const call = React.useCallback(() => {
13    if (!timeoutIdRef.current) {
14      timeoutIdRef.current = window.setTimeout(() => {
15        callbackRef.current && callbackRef.current();
16        timeoutIdRef.current = null;
17      }, delay);
18    }
19  }, [delay]);
21  const cancel = React.useCallback(() => {
22    if (timeoutIdRef.current) {
23      window.clearTimeout(timeoutIdRef.current);
24      timeoutIdRef.current = null;
25    }
26  }, []);
28  return { call, cancel };
This hook could be improved as well. For instance, we could choose to provide our consumers with the option to autoInvoke the hook without having to call it. As-is though, our useClipboard and codebase can benefit from this abstraction. Here's how to use this one:
1import React from "react";
2import { useTimeout } from "@/hooks";
4type CopyState = "READY" | "SUCCESS" | Error;
6export const useClipboard = ({ delay = 2000 } = {}) => {
7  const [state, setState] = React.useState<CopyState>("READY");
8  /* ⬇️ We consume our hook passing the needed params */
9  const timeout = useTimeout(() => setState("READY"), delay);
11  function handleCopyResult(result: CopyState) {
12    setState(result);
13    /* ⬇️ We call the timer and leave the rest to the hook */
14    timeout.call();
15  }
17  function copy(valueToCopy: string) {
18    if ("clipboard" in navigator) {
19      navigator.clipboard
20        .writeText(valueToCopy)
21        .then(() => handleCopyResult("SUCCESS"))
22        .catch((error) => error instanceof Error && handleCopyResult(error));
23    } else {
24      handleCopyResult(
25        new Error("`useClipboard`: Navigation Clipboard is not supported")
26      );
27    }
28  }
30  return { copy, state };

Wrapping up

Learning how to write a custom React Hook can give you several benefits:
  • Reusability: extract and reuse common logic in your application. By creating a custom hook, you can encapsulate complex functionality into a reusable unit that can be easily shared across different components and projects.
  • Abstraction: keep implementation details and focus on the high-level behavior of your component. This can help you write more declarative code and make your components easier to understand and maintain.
  • Composition: enable the possibility to combine multiple hooks together to create more complex functionality. This can give you the advantage of composition patterns to build more powerful and flexible components.
  • Better performance: reduce the amount of redundant code and avoid unnecessary re-renders.
  • Improved code organization: organize your code more effectively by separating concerns and promoting a more modular architecture. Create a clear separation of concerns between your components and hooks, which can make your code easier to reason about and maintain over time.
The main takeaway here is that learning how to write custom React Hooks can help you write cleaner, more modular, and more reusable code, which can save you time and effort in the long run.
Check out the code for this implementation on hectorsosa.me/hooks. How would you design this hook? Is there anything we've missed? Let us know at @_ekqt.
For more articles and full resources visit Webscope's Blog.
This article references:
Share post

Let’s stay connected

Do you want the latest and greatest from our blog straight to your inbox? Chuck us your email address and get informed.
You can unsubscribe any time. For more details, review our privacy policy