import { Component } from 'react'; import type { ErrorInfo, ReactNode } from 'react'; type FallbackArgs = { error: Error; reset: () => void }; type ErrorBoundaryProps = { children: ReactNode; /** Rendered in place of the children once something below has thrown. */ fallback: (args: FallbackArgs) => ReactNode; /** * Clear the error whenever any of these changes — pass the pathname to recover on navigation, or the * id of the thing being rendered. Without it a boundary latches: the screen stays broken until reload * even after the user has navigated somewhere that would render fine. */ resetKeys?: readonly unknown[]; onError?: (error: Error, info: ErrorInfo) => void; }; type ErrorBoundaryState = { error: Error | null }; /** * The repo had none of these. A single malformed value anywhere below the router took the whole app to a * white screen, and the only recovery from a bad stored layout was SQL — which is the wrong thing to ask * of someone who has just opened the dashboard to read what happened overnight. * * Deliberately a render-prop rather than a fixed panel: what "recover" means is different for a screen * (go somewhere else) and for one panel inside a workspace (clear the app, keep the rest running). */ export class ErrorBoundary extends Component { override state: ErrorBoundaryState = { error: null }; static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { error }; } override componentDidCatch(error: Error, info: ErrorInfo): void { console.error('[error-boundary]', error, info.componentStack); this.props.onError?.(error, info); } override componentDidUpdate(prev: ErrorBoundaryProps): void { if (!this.state.error) return; const a = prev.resetKeys ?? []; const b = this.props.resetKeys ?? []; if (a.length !== b.length || a.some((key, i) => !Object.is(key, b[i]))) this.setState({ error: null }); } reset = (): void => this.setState({ error: null }); override render(): ReactNode { const { error } = this.state; if (error) return this.props.fallback({ error, reset: this.reset }); return this.props.children; } }