Skip to content

Commit

Permalink
dev-overlay: Implement CopyButton without useActionState or async tra…
Browse files Browse the repository at this point in the history
…nsitions (#69494)

The implementation for React 18 needs to manually track the pending state. We'll only use the old implementation if React 18 is installed
  • Loading branch information
eps1lon authored Sep 4, 2024
1 parent 1b89d7d commit 2aa2bca
Showing 1 changed file with 116 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -1,28 +1,86 @@
import * as React from 'react'

type CopyState =
| {
state: 'initial'
function useCopyLegacy(content: string) {
type CopyState =
| {
state: 'initial'
}
| {
state: 'error'
error: unknown
}
| { state: 'success' }
| { state: 'pending' }

// This would be simpler with useActionState but we need to support React 18 here.
// React 18 also doesn't have async transitions.
const [copyState, dispatch] = React.useReducer(
(
state: CopyState,
action:
| { type: 'reset' | 'copied' | 'copying' }
| { type: 'error'; error: unknown }
): CopyState => {
if (action.type === 'reset') {
return { state: 'initial' }
}
if (action.type === 'copied') {
return { state: 'success' }
}
if (action.type === 'copying') {
return { state: 'pending' }
}
if (action.type === 'error') {
return { state: 'error', error: action.error }
}
return state
},
{
state: 'initial',
}
| {
state: 'error'
error: unknown
)
function copy() {
if (isPending) {
return
}
| { state: 'success' }

export function CopyButton({
actionLabel,
successLabel,
content,
icon,
disabled,
...props
}: React.HTMLProps<HTMLButtonElement> & {
actionLabel: string
successLabel: string
content: string
icon?: React.ReactNode
}) {
if (!navigator.clipboard) {
dispatch({
type: 'error',
error: new Error('Copy to clipboard is not supported in this browser'),
})
} else {
dispatch({ type: 'copying' })
navigator.clipboard.writeText(content).then(
() => {
dispatch({ type: 'copied' })
},
(error) => {
dispatch({ type: 'error', error })
}
)
}
}
const reset = React.useCallback(() => {
dispatch({ type: 'reset' })
}, [])

const isPending = copyState.state === 'pending'

return [copyState, copy, reset, isPending] as const
}

function useCopyModern(content: string) {
type CopyState =
| {
state: 'initial'
}
| {
state: 'error'
error: unknown
}
| { state: 'success' }

const [copyState, dispatch, isPending] = React.useActionState(
(
state: CopyState,
Expand Down Expand Up @@ -56,6 +114,41 @@ export function CopyButton({
}
)

function copy() {
React.startTransition(() => {
dispatch('copy')
})
}

const reset = React.useCallback(() => {
dispatch('reset')
}, [
// TODO: `dispatch` from `useActionState` is not reactive.
// Remove from dependencies once https://github.com/facebook/react/pull/29665 is released.
dispatch,
])

return [copyState, copy, reset, isPending] as const
}

const useCopy =
typeof React.useActionState === 'function' ? useCopyModern : useCopyLegacy

export function CopyButton({
actionLabel,
successLabel,
content,
icon,
disabled,
...props
}: React.HTMLProps<HTMLButtonElement> & {
actionLabel: string
successLabel: string
content: string
icon?: React.ReactNode
}) {
const [copyState, copy, reset, isPending] = useCopy(content)

const error = copyState.state === 'error' ? copyState.error : null
React.useEffect(() => {
if (error !== null) {
Expand All @@ -66,20 +159,14 @@ export function CopyButton({
React.useEffect(() => {
if (copyState.state === 'success') {
const timeoutId = setTimeout(() => {
dispatch('reset')
reset()
}, 2000)

return () => {
clearTimeout(timeoutId)
}
}
}, [
isPending,
copyState.state,
// TODO: `dispatch` from `useActionState` is not reactive.
// Remove from dependencies once https://github.com/facebook/react/pull/29665 is released.
dispatch,
])
}, [isPending, copyState.state, reset])
const isDisabled = isPending || disabled
const label = copyState.state === 'success' ? successLabel : actionLabel

Expand All @@ -98,9 +185,7 @@ export function CopyButton({
className={`nextjs-data-runtime-error-copy-button nextjs-data-runtime-error-copy-button--${copyState.state}`}
onClick={() => {
if (!isDisabled) {
React.startTransition(() => {
dispatch('copy')
})
copy()
}
}}
>
Expand Down

0 comments on commit 2aa2bca

Please sign in to comment.