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