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:
Hein
2026-02-02 13:15:13 +02:00
parent ad2252f5e4
commit a62036bb5a
3 changed files with 312 additions and 0 deletions

View 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;

View 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;

View File

@@ -0,0 +1,2 @@
export { default as ReactBasicErrorBoundary } from './BasicErrorBoundary';
export { default as ReactErrorBoundary } from './ErrorBoundary';