feat(ErrorBoundary): ✨ add ReactBasicErrorBoundary and ReactErrorBoundary components
* Implemented ReactBasicErrorBoundary for error handling. * Created ReactErrorBoundary with enhanced error reporting features. * Updated index file to export both components.
This commit is contained in:
70
src/ErrorBoundary/BasicErrorBoundary.tsx
Normal file
70
src/ErrorBoundary/BasicErrorBoundary.tsx
Normal file
@@ -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 (
|
||||||
|
<div>
|
||||||
|
<h2>Error</h2>
|
||||||
|
{this.state.error && (
|
||||||
|
<>
|
||||||
|
<h3>In: {this.props.namespace ?? 'default'}</h3>
|
||||||
|
<main>{this.state.error.toString()}</main>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normally, just render children
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReactBasicErrorBoundary;
|
||||||
240
src/ErrorBoundary/ErrorBoundary.tsx
Normal file
240
src/ErrorBoundary/ErrorBoundary.tsx
Normal file
@@ -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<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) {
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
style={{
|
||||||
|
color: 'var(--mantine-color-error)',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconExclamationCircle
|
||||||
|
color="var(--mantine-color-error)"
|
||||||
|
style={{ height: rem(20), width: rem(20) }}
|
||||||
|
/>
|
||||||
|
A react error has occurred!
|
||||||
|
</h2>
|
||||||
|
<h4>You can try one of these solutions to get back on track:</h4>
|
||||||
|
<ul>
|
||||||
|
<li>Report the error to our team</li>
|
||||||
|
<li>Hit the keys Ctrl-Shift-R</li>
|
||||||
|
<li>Make sure your web browser is up to date</li>
|
||||||
|
<li>Clear your browser cache and cookies</li>
|
||||||
|
<li>Try using a different web browser</li>
|
||||||
|
<li>Reset your layout and settings</li>
|
||||||
|
<li>Retry loading the application</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<Button
|
||||||
|
color="red"
|
||||||
|
my="md"
|
||||||
|
onClick={() =>
|
||||||
|
this.setState((state) => ({
|
||||||
|
showDetail: !state.showDetail,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
size="compact-xs"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
More
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="blue"
|
||||||
|
disabled={this.state.tryCnt > 2 || this.state.try}
|
||||||
|
my="md"
|
||||||
|
onClick={() => this.retry()}
|
||||||
|
size="compact-xs"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Retry {this.state.tryCnt > 0 ? `(${this.state.tryCnt})` : ''}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="yellow"
|
||||||
|
disabled={this.state.wasReset}
|
||||||
|
my="md"
|
||||||
|
onClick={() => this.reset()}
|
||||||
|
size="compact-xs"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Reset Layout or Settings
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="green"
|
||||||
|
disabled={this.state.reported}
|
||||||
|
my="md"
|
||||||
|
onClick={() => this.report()}
|
||||||
|
size="compact-xs"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Report
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{this.state.error && (
|
||||||
|
<Paper
|
||||||
|
p="md"
|
||||||
|
shadow="xs"
|
||||||
|
style={{
|
||||||
|
textAlign: 'justify',
|
||||||
|
}}
|
||||||
|
w="50%"
|
||||||
|
withBorder
|
||||||
|
>
|
||||||
|
{this.props.namespace && <Text size="sm">In: {this.props.namespace}</Text>}
|
||||||
|
<Text size="sm">{this.state.error.toString()}</Text>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Collapse in={this.state.showDetail}>
|
||||||
|
<Code block color="rgba(235, 86, 5, 0.6)" mah="100vh" w="100%">
|
||||||
|
{this.state.errorInfo.componentStack}
|
||||||
|
</Code>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//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 <div>Retrying {this.state.tryCnt}...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
2
src/ErrorBoundary/index.ts
Normal file
2
src/ErrorBoundary/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as ReactBasicErrorBoundary } from './BasicErrorBoundary';
|
||||||
|
export { default as ReactErrorBoundary } from './ErrorBoundary';
|
||||||
Reference in New Issue
Block a user