Introduction
In this blog post I am going to demonstrate how to create a copy-to-clipboard functionality using React and TypeScript.
For this purpose I implement a simple application. This application displays one button. When clicked, the current number of milliseconds elapsed since the epoch gets copied to the clipboard.
Planning
Usually a normal function would be sufficient for this kind of functionality. But I also want to display a notification to the user, stating that the copy process was successful or failed. Therefore, I decided to create a reusable useCopyToClipboard
hook.
Here is a list of requirements the hook must cover:
- Provides core logic for copying a text to the clipboard
- On success, it returns the copied text (returns
undefined
otherwise) - On error, it returns an error message (returns
undefined
otherwise) - After copy, the hook resets itself
- Reset time is configurable
- Copied text is
undefined
after reset - Error message is
undefined
after reset
Based on these requirements the hook has the following API:
const { copyToClipboard, copiedText, error } = copyToClipboard(2000);
The core functionality is exposed by copyToClipboard
. Whenever copiedText
is defined, the copy process was successful. When error
is defined, something went wrong. 2000
milliseconds after copying, everything will be resetted.
Implementation
TL;DR: Final implementation
In the following section I am going to implement the hook step by step:
- Copy-to-clipboard browser APIs
- Core copy-to-clipboard functionality
- Handle errors
- Reset the hook
- Final implementation
Copy-to-clipboard browser APIs
There are 2 browser APIs available to copy a text to the clipboard, the deprecated document.execCommand('copy', ...)
and the new navigator.clipboard.writeText(...)
. To support a wide range of browsers, both will be used. Unfortunately, those APIs work differently. document.execCommand('copy', ...)
returns a boolean indicating if copying was successful or not. navigator.clipboard.writeText(...)
is an async function returning Promise<void>
. Create a wrapper function to unify both:
const writeClipboardContent = async (text: string) => {
if (!navigator.clipboard) {
return document.execCommand('copy', false, text);
}
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
return false;
}
}
If navigation.clipboard
is not available, document.execCommand('copy', ...)
is used as a fallback. In both cases Promise<boolean>
is returned.
Core copy-to-clipboard functionality
Now let’s create the first hook draft. This version contains the basic copy functionality and returns the copied text:
import { useState } from "react";
const writeClipboardContent = async (text: string) => {
if (!navigator.clipboard) {
return document.execCommand('copy', false, text);
}
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
return false;
}
}
const useCopyToClipboard = () => {
const [copiedText, setCopiedText] = useState<string>();
const copyToClipboard = async (text: string) => {
if (await writeClipboardContent(text)) {
setCopiedText(text);
}
};
return {
copyToClipboard,
copiedText,
};
};
Handle errors
For handling errors, introduce an error
state. This will be set and returned on failure:
import { useState } from "react";
const writeClipboardContent = async (text: string) => {
if (!navigator.clipboard) {
return document.execCommand('copy', false, text);
}
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
return false;
}
}
const useCopyToClipboard = () => {
const [copiedText, setCopiedText] = useState<string>();
const [error, setError] = useState<string>();
const copyToClipboard = async (text: string) => {
if (await writeClipboardContent(text)) {
setCopiedText(text);
setError(undefined);
} else {
setCopiedText(undefined);
setError('Copy to clipboard failed');
}
};
return {
copyToClipboard,
copiedText,
error,
};
};
Reset the hook
When using the hook, the corresponding state will be set but not unset afterwards. Add a parameter that defines the reset time (default value: 2000 milliseconds). Furthermore use useRef
to create a reset timeout. This will be set whenever something gets copied to the clipboard, resetTimeout.current = setTimeout(handleReset, resetTime)
. When you click the button again, before the reset timeout ran out, the timeout will be restarted. We also extract the state handling into dedicated functions:
import { useRef, useState } from "react";
const RESET_TIME_IN_MILLISECONDS = 2000;
const writeClipboardContent = async (text: string) => {
if (!navigator.clipboard) {
return document.execCommand('copy', false, text);
}
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
return false;
}
}
const useCopyToClipboard = (resetTime = RESET_TIME_IN_MILLISECONDS) => {
const [copiedText, setCopiedText] = useState<string>();
const [error, setError] = useState<string>();
const resetTimeout = useRef<NodeJS.Timeout>();
const handleSuccesfulCopy = (copiedText: string) => {
setCopiedText(copiedText);
setError(undefined);
};
const handleFailedCopy = (error: string) => {
setCopiedText(undefined);
setError(error);
};
const handleReset = () => {
setCopiedText(undefined);
setError(undefined);
};
const copyToClipboard = async (text: string) => {
if (resetTimeout.current) {
clearTimeout(resetTimeout.current);
}
(await writeClipboardContent(text))
? handleSuccesfulCopy(text)
: handleFailedCopy("Copy to clipboard failed");
resetTimeout.current = setTimeout(handleReset, resetTime);
};
return {
copyToClipboard,
copiedText,
error,
};
};
Final implementation
With the reset all hook requirements are fulfilled. As a final step use useCopyToClipboard
as part of the example application described in the introduction:
import { useRef, useState } from "react";
const RESET_TIME_IN_MILLISECONDS = 2000;
const writeClipboardContent = async (text: string) => {
if (!navigator.clipboard) {
return document.execCommand("copy", false, text);
}
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
return false;
}
};
const useCopyToClipboard = (resetTime = RESET_TIME_IN_MILLISECONDS) => {
const [copiedText, setCopiedText] = useState<string>();
const [error, setError] = useState<string>();
const resetTimeout = useRef<NodeJS.Timeout>();
const handleSuccesfulCopy = (copiedText: string) => {
setCopiedText(copiedText);
setError(undefined);
};
const handleFailedCopy = (error: string) => {
setCopiedText(undefined);
setError(error);
};
const handleReset = () => {
setCopiedText(undefined);
setError(undefined);
};
const copyToClipboard = async (text: string) => {
if (resetTimeout.current) {
clearTimeout(resetTimeout.current);
}
(await writeClipboardContent(text))
? handleSuccesfulCopy(text)
: handleFailedCopy("Copy to clipboard failed");
resetTimeout.current = setTimeout(handleReset, resetTime);
};
return {
copyToClipboard,
copiedText,
error,
};
};
const App = () => {
const { copyToClipboard, copiedText, error } = useCopyToClipboard();
return (
<div>
<button onClick={() => copyToClipboard(Date.now().toString())}>Copy to clipboard</button>
{copiedText && <p style={{ color: "green" }}>Copy to clipboard succeeded</p>}
{error && <p style={{ color: "red" }}>{error}</p>}
</div>
);
}
export default App;
As you can see, the App
component displays a button which triggers the copy process. It also shows a success- or error-message based on what the hook returns.
Conclusion
That’s it. In this blog post I described how to implement a copy-to-clipboard functionality using React and TypeScript. For this purpose I created a hook that is also capable of handling errors and resetting itself.