diff --git a/src/ErrorBoundary/BasicErrorBoundary.tsx b/src/ErrorBoundary/BasicErrorBoundary.tsx new file mode 100644 index 0000000..80e76f1 --- /dev/null +++ b/src/ErrorBoundary/BasicErrorBoundary.tsx @@ -0,0 +1,70 @@ +import React, { type PropsWithChildren } from 'react'; + +interface ErrorBoundaryProps extends PropsWithChildren { + namespace?: string; + onReportClick?: () => void; + onResetClick?: () => void; + onRetryClick?: () => void; + reportAPI?: string; +} + +interface ErrorBoundaryState { + error: any; + errorInfo: any; + reported?: boolean; + resetted?: boolean; + showDetail: boolean; + timer?: NodeJS.Timeout | undefined; + try: boolean; + tryCnt: number; +} + +export class ReactBasicErrorBoundary extends React.PureComponent< + ErrorBoundaryProps, + ErrorBoundaryState +> { + constructor(props: ErrorBoundaryProps) { + super(props); + + this.state = { + error: null, + errorInfo: null, + showDetail: false, + timer: undefined, + try: false, + tryCnt: 0, + }; + } + + componentDidCatch(error: any, errorInfo: any) { + // Catch errors in any components below and re-render with error message + this.setState({ + error, + errorInfo, + try: false, + }); + // You can also log error messages to an error reporting service here + } + + render() { + if (this.state.errorInfo) { + // Error path + return ( +
+

Error

+ {this.state.error && ( + <> +

In: {this.props.namespace ?? 'default'}

+
{this.state.error.toString()}
+ + )} +
+ ); + } + + // Normally, just render children + return this.props.children; + } +} + +export default ReactBasicErrorBoundary; diff --git a/src/ErrorBoundary/ErrorBoundary.tsx b/src/ErrorBoundary/ErrorBoundary.tsx new file mode 100644 index 0000000..0ee6251 --- /dev/null +++ b/src/ErrorBoundary/ErrorBoundary.tsx @@ -0,0 +1,240 @@ +import { Button, Code, Collapse, Group, Paper, rem, Text } from '@mantine/core'; +import { IconExclamationCircle } from '@tabler/icons-react'; +import React, { type PropsWithChildren } from 'react'; + +let ErrorBoundaryOptions = { + disabled: false, + onError: undefined, +} as { + disabled?: boolean; + onError?: (error: any, errorInfo: any) => void; +}; + +export const SetErrorBoundaryOptions = (options: typeof ErrorBoundaryOptions) => { + ErrorBoundaryOptions = { ...ErrorBoundaryOptions, ...options }; +}; + +export const GetErrorBoundaryOptions = () => { + return { ...ErrorBoundaryOptions }; +}; + +interface ErrorBoundaryProps extends PropsWithChildren { + namespace?: string; + onReportClick?: () => void; + onResetClick?: () => void; + onRetryClick?: () => void; + reportAPI?: string; +} + +interface ErrorBoundaryState { + error: any; + errorInfo: any; + reported?: boolean; + showDetail: boolean; + timer?: NodeJS.Timeout | undefined; + try: boolean; + tryCnt: number; + wasReset?: boolean; +} + +export class ReactErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { + super(props); + + this.state = { + error: null, + errorInfo: null, + showDetail: false, + timer: undefined, + try: false, + tryCnt: 0, + }; + } + + componentDidCatch(error: any, errorInfo: any) { + if (GetErrorBoundaryOptions()?.disabled) { + throw Error('ErrorBoundary pass through enabled, rethrowing error', { + cause: error, + //@ts-ignore + info: errorInfo, + }); + } + // Catch errors in any components below and re-render with error message + this.setState({ + error, + errorInfo, + try: false, + }); + if (typeof GetErrorBoundaryOptions()?.onError === 'function') { + GetErrorBoundaryOptions()?.onError?.(error, errorInfo); + } + // You can also log error messages to an error reporting service here + } + + render() { + if (this.state.errorInfo) { + // Error path + return ( +
+

+ + A react error has occurred! +

+

You can try one of these solutions to get back on track:

+ + + + + + + + + + {this.state.error && ( + + {this.props.namespace && In: {this.props.namespace}} + {this.state.error.toString()} + + )} + + + + {this.state.errorInfo.componentStack} + + +
+ ); + } + + //Retry code, if you do it too many times, it will stop + if (this.state.try) { + if (!this.state.timer) { + const tm = setTimeout(() => { + clearTimeout(this.state.timer); + this.setState((state) => ({ + ...state, + timer: undefined, + try: false, + })); + }, 1000); + + this.setState((state) => ({ + ...state, + timer: tm, + })); + } + return
Retrying {this.state.tryCnt}...
; + } + + // Normally, just render children + return this.props.children; + } + + async report() { + if (this.props?.onReportClick) { + this.props.onReportClick(); + this.setState(() => ({ + reported: true, + })); + return; + } + } + + reset() { + if (this.props?.onResetClick) { + this.props.onResetClick(); + this.setState(() => ({ + wasReset: true, + })); + return; + } + + this.setState(() => ({ + wasReset: true, + })); + + window.location.reload(); + } + + retry() { + this.setState({ + try: true, + tryCnt: this.state.tryCnt + 1, + }); + if (this.props?.onRetryClick) { + this.props.onRetryClick(); + return; + } + this.setState({ + error: undefined, + errorInfo: undefined, + }); + this.forceUpdate(); + } +} + +export default ReactErrorBoundary; diff --git a/src/ErrorBoundary/index.ts b/src/ErrorBoundary/index.ts new file mode 100644 index 0000000..9d56e81 --- /dev/null +++ b/src/ErrorBoundary/index.ts @@ -0,0 +1,2 @@ +export { default as ReactBasicErrorBoundary } from './BasicErrorBoundary'; +export { default as ReactErrorBoundary } from './ErrorBoundary';