Skip to content

Commit dbf8ea3

Browse files
committed
dev-overlay: Implement CopyButton without useActionState or async transitions
This doesn't have the same fidelity as with `useActionState` and probably more bugs. We'll only use the old implementation if React 18 is installed
1 parent 0da034f commit dbf8ea3

File tree

1 file changed

+113
-28
lines changed
  • packages/next/src/client/components/react-dev-overlay/internal/components/copy-button

1 file changed

+113
-28
lines changed

packages/next/src/client/components/react-dev-overlay/internal/components/copy-button/index.tsx

Lines changed: 113 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,86 @@
11
import * as React from 'react'
22

3-
type CopyState =
4-
| {
5-
state: 'initial'
3+
function useCopyLegacy(content: string) {
4+
type CopyState =
5+
| {
6+
state: 'initial'
7+
}
8+
| {
9+
state: 'error'
10+
error: unknown
11+
}
12+
| { state: 'success' }
13+
| { state: 'pending' }
14+
15+
// This would be simpler with useActionState but we need to support React 18 here.
16+
// React 18 also doesn't have async transitions.
17+
const [copyState, dispatch] = React.useReducer(
18+
(
19+
state: CopyState,
20+
action:
21+
| { type: 'reset' | 'copied' | 'copying' }
22+
| { type: 'error'; error: unknown }
23+
): CopyState => {
24+
if (action.type === 'reset') {
25+
return { state: 'initial' }
26+
}
27+
if (action.type === 'copied') {
28+
return { state: 'success' }
29+
}
30+
if (action.type === 'copying') {
31+
return { state: 'pending' }
32+
}
33+
if (action.type === 'error') {
34+
return { state: 'error', error: action.error }
35+
}
36+
return state
37+
},
38+
{
39+
state: 'initial',
40+
}
41+
)
42+
function copy() {
43+
if (isPending) {
44+
return
645
}
7-
| {
8-
state: 'error'
9-
error: unknown
46+
47+
if (!navigator.clipboard) {
48+
dispatch({
49+
type: 'error',
50+
error: new Error('Copy to clipboard is not supported in this browser'),
51+
})
52+
} else {
53+
dispatch({ type: 'copying' })
54+
navigator.clipboard.writeText(content).then(
55+
() => {
56+
dispatch({ type: 'copied' })
57+
},
58+
(error) => {
59+
dispatch({ type: 'error', error })
60+
}
61+
)
1062
}
11-
| { state: 'success' }
63+
}
64+
const reset = React.useCallback(() => {
65+
dispatch({ type: 'reset' })
66+
}, [])
67+
68+
const isPending = copyState.state === 'pending'
69+
70+
return [copyState, copy, reset, isPending] as const
71+
}
72+
73+
function useCopyModern(content: string) {
74+
type CopyState =
75+
| {
76+
state: 'initial'
77+
}
78+
| {
79+
state: 'error'
80+
error: unknown
81+
}
82+
| { state: 'success' }
1283

13-
export function CopyButton({
14-
actionLabel,
15-
successLabel,
16-
content,
17-
...props
18-
}: React.HTMLProps<HTMLButtonElement> & {
19-
actionLabel: string
20-
successLabel: string
21-
content: string
22-
}) {
2384
const [copyState, dispatch, isPending] = React.useActionState(
2485
(
2586
state: CopyState,
@@ -53,6 +114,38 @@ export function CopyButton({
53114
}
54115
)
55116

117+
function copy() {
118+
React.startTransition(() => {
119+
dispatch('copy')
120+
})
121+
}
122+
123+
const reset = React.useCallback(() => {
124+
dispatch('reset')
125+
}, [
126+
// TODO: `dispatch` from `useActionState` is not reactive.
127+
// Remove from dependencies once https://github.com/facebook/react/pull/29665 is released.
128+
dispatch,
129+
])
130+
131+
return [copyState, copy, reset, isPending] as const
132+
}
133+
134+
const useCopy =
135+
typeof React.useActionState === 'function' ? useCopyModern : useCopyLegacy
136+
137+
export function CopyButton({
138+
actionLabel,
139+
successLabel,
140+
content,
141+
...props
142+
}: React.HTMLProps<HTMLButtonElement> & {
143+
actionLabel: string
144+
successLabel: string
145+
content: string
146+
}) {
147+
const [copyState, copy, reset, isPending] = useCopy(content)
148+
56149
const error = copyState.state === 'error' ? copyState.error : null
57150
React.useEffect(() => {
58151
if (error !== null) {
@@ -63,20 +156,14 @@ export function CopyButton({
63156
React.useEffect(() => {
64157
if (copyState.state === 'success') {
65158
const timeoutId = setTimeout(() => {
66-
dispatch('reset')
159+
reset()
67160
}, 2000)
68161

69162
return () => {
70163
clearTimeout(timeoutId)
71164
}
72165
}
73-
}, [
74-
isPending,
75-
copyState.state,
76-
// TODO: `dispatch` from `useActionState` is not reactive.
77-
// Remove from dependencies once https://github.com/facebook/react/pull/29665 is released.
78-
dispatch,
79-
])
166+
}, [isPending, copyState.state, reset])
80167
const isDisabled = isPending
81168
const label = copyState.state === 'success' ? successLabel : actionLabel
82169
const title = label
@@ -94,9 +181,7 @@ export function CopyButton({
94181
className={`nextjs-data-runtime-error-copy-stack nextjs-data-runtime-error-copy-stack--${copyState.state}`}
95182
onClick={() => {
96183
if (!isDisabled) {
97-
React.startTransition(() => {
98-
dispatch('copy')
99-
})
184+
copy()
100185
}
101186
}}
102187
>

0 commit comments

Comments
 (0)