222 lines
6.1 KiB
TypeScript
222 lines
6.1 KiB
TypeScript
import { Router } from '../client/router'
|
|
import { RedirectError } from './errors'
|
|
import * as React from 'react'
|
|
import { RouterContext } from '../shared/lib/router-context'
|
|
const debug = require('debug')('blitz:errorboundary')
|
|
|
|
const changedArray = (a: Array<unknown> = [], b: Array<unknown> = []) =>
|
|
a.length !== b.length || a.some((item, index) => !Object.is(item, b[index]))
|
|
|
|
interface ErrorFallbackProps {
|
|
error: Error & Record<any, any>
|
|
resetErrorBoundary: (...args: Array<unknown>) => void
|
|
}
|
|
|
|
interface ErrorBoundaryPropsWithComponent {
|
|
onResetKeysChange?: (
|
|
prevResetKeys: Array<unknown> | undefined,
|
|
resetKeys: Array<unknown> | undefined
|
|
) => void
|
|
onReset?: (...args: Array<unknown>) => void
|
|
onError?: (error: Error, info: { componentStack: string }) => void
|
|
resetKeys?: Array<unknown>
|
|
fallback?: never
|
|
FallbackComponent: React.ComponentType<ErrorFallbackProps>
|
|
fallbackRender?: never
|
|
}
|
|
|
|
declare function FallbackRender(
|
|
props: ErrorFallbackProps
|
|
): React.ReactElement<
|
|
unknown,
|
|
string | React.FunctionComponent | typeof React.Component
|
|
> | null
|
|
|
|
interface ErrorBoundaryPropsWithRender {
|
|
onResetKeysChange?: (
|
|
prevResetKeys: Array<unknown> | undefined,
|
|
resetKeys: Array<unknown> | undefined
|
|
) => void
|
|
onReset?: (...args: Array<unknown>) => void
|
|
onError?: (error: Error, info: { componentStack: string }) => void
|
|
resetKeys?: Array<unknown>
|
|
fallback?: never
|
|
FallbackComponent?: never
|
|
fallbackRender: typeof FallbackRender
|
|
}
|
|
|
|
interface ErrorBoundaryPropsWithFallback {
|
|
onResetKeysChange?: (
|
|
prevResetKeys: Array<unknown> | undefined,
|
|
resetKeys: Array<unknown> | undefined
|
|
) => void
|
|
onReset?: (...args: Array<unknown>) => void
|
|
onError?: (error: Error, info: { componentStack: string }) => void
|
|
resetKeys?: Array<unknown>
|
|
fallback: React.ReactElement<
|
|
unknown,
|
|
string | React.FunctionComponent | typeof React.Component
|
|
> | null
|
|
FallbackComponent?: never
|
|
fallbackRender?: never
|
|
}
|
|
|
|
type ErrorBoundaryProps =
|
|
| ErrorBoundaryPropsWithFallback
|
|
| ErrorBoundaryPropsWithComponent
|
|
| ErrorBoundaryPropsWithRender
|
|
|
|
type ErrorBoundaryState = { error: Error | null }
|
|
|
|
const initialState: ErrorBoundaryState = { error: null }
|
|
|
|
class ErrorBoundary extends React.Component<
|
|
React.PropsWithRef<React.PropsWithChildren<ErrorBoundaryProps>>,
|
|
ErrorBoundaryState
|
|
> {
|
|
static contextType = RouterContext
|
|
|
|
static getDerivedStateFromError(error: Error) {
|
|
return { error }
|
|
}
|
|
|
|
state = initialState
|
|
updatedWithError = false
|
|
resetErrorBoundary = (...args: Array<unknown>) => {
|
|
this.props.onReset?.(...args)
|
|
this.reset()
|
|
}
|
|
|
|
reset() {
|
|
this.updatedWithError = false
|
|
this.setState(initialState)
|
|
}
|
|
|
|
async componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
if (error instanceof RedirectError) {
|
|
debug('Redirecting from ErrorBoundary to', error.url)
|
|
await (this.context as Router)?.push(error.url)
|
|
return
|
|
}
|
|
this.props.onError?.(error, info)
|
|
}
|
|
|
|
componentDidMount() {
|
|
const { error } = this.state
|
|
|
|
if (error !== null) {
|
|
this.updatedWithError = true
|
|
}
|
|
|
|
// Automatically reset on route change
|
|
;(this.context as Router)?.events?.on(
|
|
'routeChangeComplete',
|
|
this.handleRouteChange
|
|
)
|
|
}
|
|
|
|
handleRouteChange = () => {
|
|
debug('Resetting error boundary on route change')
|
|
this.props.onReset?.()
|
|
this.reset()
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
;(this.context as Router)?.events?.off(
|
|
'routeChangeComplete',
|
|
this.handleRouteChange
|
|
)
|
|
}
|
|
|
|
componentDidUpdate(prevProps: ErrorBoundaryProps) {
|
|
const { error } = this.state
|
|
const { resetKeys } = this.props
|
|
|
|
// There's an edge case where if the thing that triggered the error
|
|
// happens to *also* be in the resetKeys array, we'd end up resetting
|
|
// the error boundary immediately. This would likely trigger a second
|
|
// error to be thrown.
|
|
// So we make sure that we don't check the resetKeys on the first call
|
|
// of cDU after the error is set
|
|
if (error !== null && !this.updatedWithError) {
|
|
this.updatedWithError = true
|
|
return
|
|
}
|
|
|
|
if (error !== null && changedArray(prevProps.resetKeys, resetKeys)) {
|
|
this.props.onResetKeysChange?.(prevProps.resetKeys, resetKeys)
|
|
this.reset()
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const { error } = this.state
|
|
|
|
const { fallbackRender, FallbackComponent, fallback } = this.props
|
|
|
|
if (error !== null) {
|
|
const props = {
|
|
error,
|
|
resetErrorBoundary: this.resetErrorBoundary,
|
|
}
|
|
if (error instanceof RedirectError) {
|
|
// Don't render children because redirect is imminent
|
|
return null
|
|
} else if (React.isValidElement(fallback)) {
|
|
return fallback
|
|
} else if (typeof fallbackRender === 'function') {
|
|
return fallbackRender(props)
|
|
} else if (FallbackComponent) {
|
|
return <FallbackComponent {...props} />
|
|
} else {
|
|
throw new Error(
|
|
'<ErrorBoundary> requires either a fallback, fallbackRender, or FallbackComponent prop'
|
|
)
|
|
}
|
|
}
|
|
|
|
return this.props.children
|
|
}
|
|
}
|
|
|
|
function withErrorBoundary<P>(
|
|
Component: React.ComponentType<P>,
|
|
errorBoundaryProps: ErrorBoundaryProps
|
|
): React.ComponentType<P> {
|
|
const Wrapped: React.ComponentType<P> = (props) => {
|
|
return (
|
|
<ErrorBoundary {...errorBoundaryProps}>
|
|
<Component {...props} />
|
|
</ErrorBoundary>
|
|
)
|
|
}
|
|
|
|
// Format for display in DevTools
|
|
const name = Component.displayName || Component.name || 'Unknown'
|
|
Wrapped.displayName = `withErrorBoundary(${name})`
|
|
|
|
return Wrapped
|
|
}
|
|
|
|
function useErrorHandler(givenError?: unknown): (error: unknown) => void {
|
|
const [error, setError] = React.useState<unknown>(null)
|
|
if (givenError != null) throw givenError
|
|
if (error != null) throw error
|
|
return setError
|
|
}
|
|
|
|
export { ErrorBoundary, withErrorBoundary, useErrorHandler }
|
|
export type {
|
|
ErrorFallbackProps,
|
|
ErrorBoundaryPropsWithComponent,
|
|
ErrorBoundaryPropsWithRender,
|
|
ErrorBoundaryPropsWithFallback,
|
|
ErrorBoundaryProps,
|
|
}
|
|
|
|
/*
|
|
eslint
|
|
@typescript-eslint/no-throw-literal: "off",
|
|
@typescript-eslint/prefer-nullish-coalescing: "off"
|
|
*/
|