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 withuse
, such asuseState
. 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.
navigator.clipboard
.writeText("Text to copy")
.then(()=> /* Clipboard successfully set */)
.catch((err) => /* Clipboard write failed */);
If we go ahead and integrate this directly into a button, it could look something like this:
import React from "react";
import { CopyIcon, ErrorIcon, SuccessIcon } from "@/components/icons";
export const CopyButton = ({
textToCopy,
children
}: {
textToCopy: string,
children?: React.ReactNode
}) => {
const [clipboardState, setClipboardState] = React.useState<boolean | Error>(false);
return (
<button
onClick={() => {
navigator.clipboard
.writeText(code)
.then(() => setClipboardState(true))
.catch((e) => setClipboardState(e));
}}
>
{children}
{clipboardState ? (
clipboardState instanceof Error ? <ErrorIcon /> : <SuccessIcon />
) : (
<CopyIcon />
)}
</button>
);
};
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.
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.
import React from "react";
type CopyState = "READY" | "SUCCESS" | Error;
export const useClipboard = ({ delay = 2500 } = {}) => {
const [state, setState] = React.useState<CopyState>("READY");
const [copyTimeout, setCopyTimeout] =
React.useState<ReturnType<typeof setTimeout>>();
function handleCopyResult(result: CopyState) {
setState(result);
clearTimeout(copyTimeout);
setCopyTimeout(setTimeout(() => setState("READY"), delay));
}
function copy(valueToCopy: string) {
if ("clipboard" in navigator) {
navigator.clipboard
.writeText(valueToCopy)
.then(() => handleCopyResult("SUCCESS"))
.catch((error) => error instanceof Error && handleCopyResult(error));
} else {
handleCopyResult(
new Error("`useClipboard`: Navigation Clipboard is not supported")
);
}
}
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 usingclipboard.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
.
import React from "react";
import { CopyIcon, ErrorIcon, SuccessIcon } from "@/components/icons";
import { useClipboard } from "@/hooks";
export const CopyButton = ({
textToCopy,
children
}: {
textToCopy: string,
children?: React.ReactNode
}) => {
/* β¬οΈ Methods are now available to copy and check the hook's state */
const clipboard = useClipboard();
return (
<button onClick={() => clipboard.copy(textToCopy)}>
{children}
/* β¬οΈ Type autocompletion will make it easy to structure your UI */
{clipboard.state === "READY" && <CopyIcon />}
{clipboard.state === "SUCCESS" && <SuccessIcon />}
{clipboard.state === "ERROR" && <ErrorIcon />}
</button>
);
};
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.
import React from "react";
export const useTimeout = (callback: Function, delay: number) => {
const callbackRef = React.useRef<Function | null>(null);
const timeoutIdRef = React.useRef<number | null>(null);
/* β¬οΈ Only executed during initialization */
if (!callbackRef.current) {
callbackRef.current = callback;
}
const call = React.useCallback(() => {
if (!timeoutIdRef.current) {
timeoutIdRef.current = window.setTimeout(() => {
callbackRef.current && callbackRef.current();
timeoutIdRef.current = null;
}, delay);
}
}, [delay]);
const cancel = React.useCallback(() => {
if (timeoutIdRef.current) {
window.clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = null;
}
}, []);
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:
import React from "react";
import { useTimeout } from "@/hooks";
type CopyState = "READY" | "SUCCESS" | Error;
export const useClipboard = ({ delay = 2000 } = {}) => {
const [state, setState] = React.useState<CopyState>("READY");
/* β¬οΈ We consume our hook passing the needed params */
const timeout = useTimeout(() => setState("READY"), delay);
function handleCopyResult(result: CopyState) {
setState(result);
/* β¬οΈ We call the timer and leave the rest to the hook */
timeout.call();
}
function copy(valueToCopy: string) {
if ("clipboard" in navigator) {
navigator.clipboard
.writeText(valueToCopy)
.then(() => handleCopyResult("SUCCESS"))
.catch((error) => error instanceof Error && handleCopyResult(error));
} else {
handleCopyResult(
new Error("`useClipboard`: Navigation Clipboard is not supported")
);
}
}
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:
Top comments (3)
Clear, concise and more than thorough. Thank you!
Detail and Impressive. Thank you!
Thanks for great article π