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 = [], b: Array = []) => a.length !== b.length || a.some((item, index) => !Object.is(item, b[index])) interface ErrorFallbackProps { error: Error & Record resetErrorBoundary: (...args: Array) => void } interface ErrorBoundaryPropsWithComponent { onResetKeysChange?: ( prevResetKeys: Array | undefined, resetKeys: Array | undefined ) => void onReset?: (...args: Array) => void onError?: (error: Error, info: { componentStack: string }) => void resetKeys?: Array fallback?: never FallbackComponent: React.ComponentType fallbackRender?: never } declare function FallbackRender( props: ErrorFallbackProps ): React.ReactElement< unknown, string | React.FunctionComponent | typeof React.Component > | null interface ErrorBoundaryPropsWithRender { onResetKeysChange?: ( prevResetKeys: Array | undefined, resetKeys: Array | undefined ) => void onReset?: (...args: Array) => void onError?: (error: Error, info: { componentStack: string }) => void resetKeys?: Array fallback?: never FallbackComponent?: never fallbackRender: typeof FallbackRender } interface ErrorBoundaryPropsWithFallback { onResetKeysChange?: ( prevResetKeys: Array | undefined, resetKeys: Array | undefined ) => void onReset?: (...args: Array) => void onError?: (error: Error, info: { componentStack: string }) => void resetKeys?: Array 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>, ErrorBoundaryState > { static contextType = RouterContext static getDerivedStateFromError(error: Error) { return { error } } state = initialState updatedWithError = false resetErrorBoundary = (...args: Array) => { 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 } else { throw new Error( ' requires either a fallback, fallbackRender, or FallbackComponent prop' ) } } return this.props.children } } function withErrorBoundary

( Component: React.ComponentType

, errorBoundaryProps: ErrorBoundaryProps ): React.ComponentType

{ const Wrapped: React.ComponentType

= (props) => { return ( ) } // 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(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" */