75 lines
2.2 KiB
TypeScript
75 lines
2.2 KiB
TypeScript
import * as React from "react";
|
|
import { ErrorState } from "@/components/ui/error-state";
|
|
|
|
interface ErrorBoundaryState {
|
|
error: Error | null;
|
|
}
|
|
|
|
interface ErrorBoundaryProps {
|
|
children: React.ReactNode;
|
|
/** Render prop for full custom fallback */
|
|
fallback?: (error: Error, reset: () => void) => React.ReactNode;
|
|
/** Reset boundary when any value in this array changes (like `useEffect` deps) */
|
|
resetKeys?: unknown[];
|
|
/** Called once when a render error is caught */
|
|
onError?: (error: Error, info: React.ErrorInfo) => void;
|
|
}
|
|
|
|
/**
|
|
* Catches render-time errors anywhere below it and shows a fallback UI.
|
|
* Wrap the whole app at the root (catches everything) and optionally
|
|
* wrap individual routes/sections for finer-grained recovery.
|
|
*
|
|
* Note: does NOT catch errors inside event handlers, async code, or
|
|
* server-side rendering. For those, surface errors via React Query
|
|
* `onError` or `toast.error()`.
|
|
*/
|
|
export class ErrorBoundary extends React.Component<
|
|
ErrorBoundaryProps,
|
|
ErrorBoundaryState
|
|
> {
|
|
state: ErrorBoundaryState = { error: null };
|
|
|
|
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
return { error };
|
|
}
|
|
|
|
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
this.props.onError?.(error, info);
|
|
// eslint-disable-next-line no-console
|
|
console.error("[ErrorBoundary]", error, info);
|
|
}
|
|
|
|
componentDidUpdate(prevProps: ErrorBoundaryProps) {
|
|
if (!this.state.error) return;
|
|
const prev = prevProps.resetKeys ?? [];
|
|
const next = this.props.resetKeys ?? [];
|
|
if (
|
|
prev.length !== next.length ||
|
|
prev.some((v, i) => !Object.is(v, next[i]))
|
|
) {
|
|
this.reset();
|
|
}
|
|
}
|
|
|
|
reset = () => this.setState({ error: null });
|
|
|
|
render() {
|
|
const { error } = this.state;
|
|
if (!error) return this.props.children;
|
|
if (this.props.fallback) return this.props.fallback(error, this.reset);
|
|
|
|
return (
|
|
<div className="mx-auto max-w-3xl px-6 py-16">
|
|
<ErrorState
|
|
severity="error"
|
|
title="Something went wrong"
|
|
description="An unexpected error broke this view. You can try reloading just this section, or refresh the page."
|
|
detail={error.stack ?? error.message}
|
|
onRetry={this.reset}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
}
|