import React from 'react'; // ───────────────────────────────────────────────────────────────────────────── // ErrorBoundary — reusable class-based error boundary // // React error boundaries MUST be class components. This is the single base // implementation used throughout the app via three pre-configured wrappers: // // — Root level: catches provider/context crashes // — Route level: catches page-level render errors // — Feature level: catches isolated section errors // // Usage: // import { PageErrorBoundary } from '@/components/shared/error-boundary'; // // ───────────────────────────────────────────────────────────────────────────── // ── Types ───────────────────────────────────────────────────────────────────── export interface FallbackProps { error: Error; reset: () => void; } interface ErrorBoundaryProps { children: React.ReactNode; /** Custom fallback UI. Receives the error and a reset callback. */ fallback: React.ComponentType; /** * Optional callback fired when an error is caught. * Use to log to Sentry, Datadog, etc. */ onError?: (error: Error, info: React.ErrorInfo) => void; /** * List of values that, when changed, automatically reset the boundary. * Useful for resetting on route changes (pass `location.pathname`). */ resetKeys?: unknown[]; } interface ErrorBoundaryState { hasError: boolean; error: Error | null; } // ── Core class ──────────────────────────────────────────────────────────────── export class ErrorBoundary extends React.Component { constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false, error: null }; this.reset = this.reset.bind(this); } static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error }; } componentDidCatch(error: Error, info: React.ErrorInfo) { this.props.onError?.(error, info); if (import.meta.env.DEV) { console.group('[ErrorBoundary] Uncaught error'); console.error(error); console.error('Component stack:', info.componentStack); console.groupEnd(); } } componentDidUpdate(prevProps: ErrorBoundaryProps) { if (!this.state.hasError) return; const { resetKeys } = this.props; if (!resetKeys?.length) return; const prevKeys = prevProps.resetKeys ?? []; const changed = resetKeys.some((key, i) => key !== prevKeys[i]); if (changed) this.reset(); } reset() { this.setState({ hasError: false, error: null }); } render() { const { hasError, error } = this.state; const { children, fallback: Fallback } = this.props; if (hasError && error) { return ; } return children; } } // ── Pre-configured variants ─────────────────────────────────────────────────── // Import the fallbacks from error-fallbacks.tsx to avoid circular deps. // These are thin wrappers so callers never need to wire up the fallback prop. import { AppErrorFallback, PageErrorFallback, SectionErrorFallback } from './error-fallbacks'; interface BoundaryProps { children: React.ReactNode; onError?: (error: Error, info: React.ErrorInfo) => void; resetKeys?: unknown[]; } /** * `AppErrorBoundary` — root level. * * Place at the very top of the tree, outside all providers. If something inside * the provider stack crashes during setup, this boundary prevents a blank screen. */ export function AppErrorBoundary({ children, onError, resetKeys }: BoundaryProps) { return ( {children} ); } /** * `PageErrorBoundary` — route level. * * Wrap `` / `` inside the router. When a lazy page chunk * fails to load, or a page component throws, the rest of the app (providers, * nav chrome) stays alive and only the page area shows the error UI. */ export function PageErrorBoundary({ children, onError, resetKeys }: BoundaryProps) { return ( {children} ); } /** * `SectionErrorBoundary` — feature/section level. * * Use around individual sections within a page (e.g., a data table, a chart, * the assistant panel). An error in one section does not crash the entire page. */ export function SectionErrorBoundary({ children, onError, resetKeys }: BoundaryProps) { return ( {children} ); }