diff --git a/README.md b/README.md
index 4bef7df8..e6ea05c9 100644
--- a/README.md
+++ b/README.md
@@ -73,6 +73,7 @@
- [**Side-effects**](./docs/Side-effects.md)
- [`useAsync`](./docs/useAsync.md) — resolves an `async` function.
- [`useAsyncRetry`](./docs/useAsyncRetry.md) — `useAsync` with `retry()` method.
+ - [`useCopyToClipboard`](./docs/useCopyToClipboard.md) — copies text to clipboard.
- [`useDebounce`](./docs/useDebounce.md) — debounces a function. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/side-effects-usedebounce--demo)
- [`useFavicon`](./docs/useFavicon.md) — sets favicon of the page.
- [`useLocalStorage`](./docs/useLocalStorage.md) — manages a value in `localStorage`.
diff --git a/docs/useCopyToClipboard.md b/docs/useCopyToClipboard.md
new file mode 100644
index 00000000..cc56dae1
--- /dev/null
+++ b/docs/useCopyToClipboard.md
@@ -0,0 +1,35 @@
+# `useCopyToClipboard`
+
+Copy text to a user's clipboard.
+
+
+## Usage
+
+Basic usage
+
+```jsx
+const Demo = () => {
+ const [text, setText] = React.useState('');
+ const [copied, copyToClipboard] = useCopyToClipboard(text);
+
+ return (
+
+
setText(e.target.value)} />
+
+
Copied: {copied ? 'Yes' : 'No'}
+
+ )
+}
+```
+
+## Reference
+
+```js
+const [copied, copyToClipboard] = useCopyToClipboard(text);
+const [copied, copyToClipboard] = useCopyToClipboard(text, writeText);
+```
+
+, where
+
+- `writeText` — function that receives a single string argument, which
+ it copies to user's clipboard.
diff --git a/package.json b/package.json
index ed73e001..6089b8d5 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
},
"homepage": "https://github.com/streamich/react-use#readme",
"dependencies": {
+ "copy-to-clipboard": "^3.1.0",
"nano-css": "^5.1.0",
"react-fast-compare": "^2.0.4",
"react-wait": "^0.3.0",
diff --git a/src/__stories__/useCopyToClipboard.story.tsx b/src/__stories__/useCopyToClipboard.story.tsx
new file mode 100644
index 00000000..7fcd5a7f
--- /dev/null
+++ b/src/__stories__/useCopyToClipboard.story.tsx
@@ -0,0 +1,27 @@
+import * as React from 'react';
+import {storiesOf} from '@storybook/react';
+import ShowDocs from './util/ShowDocs';
+import {useCopyToClipboard} from '..';
+
+const Demo = () => {
+ const [text, setText] = React.useState('');
+ const [copied, copyToClipboard] = useCopyToClipboard(text, {
+ onCopy: txt => alert('success: ' + txt),
+ onError: err => alert(err),
+ });
+
+ return (
+
+
setText(e.target.value)} />
+
+
Copied: {copied ? 'Yes' : 'No'}
+
+
+
+
+ )
+}
+
+storiesOf('Side-effects|useCopyToClipboard', module)
+ .add('Docs', () => )
+ .add('Demo', () => )
diff --git a/src/index.ts b/src/index.ts
index 272a20b7..a13d2d0f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -4,6 +4,7 @@ import useAsyncRetry from './useAsyncRetry';
import useAudio from './useAudio';
import useBattery from './useBattery';
import useBoolean from './useBoolean';
+import useCopyToClipboard from './useCopyToClipboard';
import useDrop from './useDrop';
import useDropArea from './useDropArea';
import useCounter from './useCounter';
@@ -73,6 +74,7 @@ export {
useAudio,
useBattery,
useBoolean,
+ useCopyToClipboard,
useDrop,
useDropArea,
useClickAway,
diff --git a/src/useCopyToClipboard.ts b/src/useCopyToClipboard.ts
new file mode 100644
index 00000000..244bcc1b
--- /dev/null
+++ b/src/useCopyToClipboard.ts
@@ -0,0 +1,55 @@
+import {useState, useCallback, useRef} from 'react';
+import useUpdateEffect from './useUpdateEffect';
+import useRefMounted from './useRefMounted';
+const writeTextDefault = require('copy-to-clipboard');
+
+export type WriteText = (text: string) => Promise; // https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText
+export interface UseCopyToClipboardOptions {
+ writeText?: WriteText;
+ onCopy?: (text: string) => void;
+ onError?: (error: any, text: string) => void;
+}
+export type UseCopyToClipboard = (text?: string, options?: UseCopyToClipboardOptions) => [boolean, () => void];
+
+const useCopyToClipboard: UseCopyToClipboard = (text = '', options) => {
+ const {writeText = writeTextDefault, onCopy, onError} = (options || {}) as UseCopyToClipboardOptions;
+
+ if (process.env.NODE_ENV !== 'production') {
+ if (typeof text !== 'string') {
+ console.warn('useCopyToClipboard hook expects first argument to be string.');
+ }
+ }
+
+ const mounted = useRefMounted();
+ const latestText = useRef(text);
+ const [copied, setCopied] = useState(false);
+ const copyToClipboard = useCallback(async () => {
+ if (latestText.current !== text) {
+ if (process.env.NODE_ENV !== 'production') {
+ console.warn('Trying to copy stale text.');
+ }
+ return;
+ }
+
+ try {
+ await writeText(text);
+ if (!mounted.current) return;
+ setCopied(true);
+ onCopy && onCopy(text);
+ } catch (error) {
+ if (!mounted.current) return;
+ console.error(error);
+ setCopied(false);
+ onError && onError(error, text);
+ }
+ }, [text]);
+
+ useUpdateEffect(() => {
+ setCopied(false);
+ latestText.current = text;
+ }, [text]);
+
+ return [copied, copyToClipboard];
+}
+
+export default useCopyToClipboard;
diff --git a/yarn.lock b/yarn.lock
index 75bd4d62..8219b6d2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3932,6 +3932,13 @@ copy-to-clipboard@^3.0.8:
dependencies:
toggle-selection "^1.0.3"
+copy-to-clipboard@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.1.0.tgz#0a28141899e6bd217b9dc13fd1689b3b38820b44"
+ integrity sha512-+RNyDq266tv5aGhfRsL6lxgj8Y6sCvTrVJnFUVvuxuqkcSMaLISt1wd4JkdQSphbcLTIQ9kEpTULNnoCXAFdng==
+ dependencies:
+ toggle-selection "^1.0.6"
+
core-js-compat@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.0.0.tgz#cd9810b8000742535a4a43773866185e310bd4f7"
@@ -11010,7 +11017,7 @@ to-regex@^3.0.1, to-regex@^3.0.2:
regex-not "^1.0.2"
safe-regex "^1.1.0"
-toggle-selection@^1.0.3:
+toggle-selection@^1.0.3, toggle-selection@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI=