'use client' import type { ErrorInfo, ReactNode } from 'react' import { RiAlertLine, RiBugLine } from '@remixicon/react' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import { IS_DEV } from '@/config' import { cn } from '@/utils/classnames' type ErrorBoundaryState = { hasError: boolean error: Error | null errorInfo: ErrorInfo | null errorCount: number } type ErrorBoundaryProps = { children: ReactNode fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode) onError?: (error: Error, errorInfo: ErrorInfo) => void onReset?: () => void showDetails?: boolean className?: string resetKeys?: Array resetOnPropsChange?: boolean isolate?: boolean enableRecovery?: boolean customTitle?: string customMessage?: string } type ErrorBoundaryCopy = { componentStack: string details: string error: string formatErrorCount: (count: number) => string message: string reload: string title: string tryAgain: string } // Internal class component for error catching class ErrorBoundaryInner extends React.Component< ErrorBoundaryProps & { copy: ErrorBoundaryCopy resetErrorBoundary: () => void onResetKeysChange: (prevResetKeys?: Array) => void }, ErrorBoundaryState > { constructor(props: any) { super(props) this.state = { hasError: false, error: null, errorInfo: null, errorCount: 0, } } static getDerivedStateFromError(error: Error): Partial { return { hasError: true, error, } } componentDidCatch(error: Error, errorInfo: ErrorInfo) { if (IS_DEV) { console.error('ErrorBoundary caught an error:', error) console.error('Error Info:', errorInfo) } this.setState(prevState => ({ errorInfo, errorCount: prevState.errorCount + 1, })) if (this.props.onError) this.props.onError(error, errorInfo) } componentDidUpdate(prevProps: any) { const { resetKeys, resetOnPropsChange } = this.props const { hasError } = this.state if (hasError && prevProps.resetKeys !== resetKeys) { if (resetKeys?.some((key, idx) => key !== prevProps.resetKeys?.[idx])) this.props.resetErrorBoundary() } if (hasError && resetOnPropsChange && prevProps.children !== this.props.children) this.props.resetErrorBoundary() if (prevProps.resetKeys !== resetKeys) this.props.onResetKeysChange(prevProps.resetKeys) } render() { const { hasError, error, errorInfo, errorCount } = this.state const { fallback, children, showDetails = false, className, isolate = true, enableRecovery = true, customTitle, customMessage, copy, resetErrorBoundary, } = this.props if (hasError && error) { if (fallback) { if (typeof fallback === 'function') return fallback(error, resetErrorBoundary) return fallback } return (

{customTitle || copy.title}

{customMessage || copy.message}

{showDetails && errorInfo && (
{copy.details}
{copy.error}
                    {error.toString()}
                  
{errorInfo && (
{copy.componentStack}
                      {errorInfo.componentStack}
                    
)} {errorCount > 1 && (
{copy.formatErrorCount(errorCount)}
)}
)} {enableRecovery && (
)}
) } return children } } // Main functional component wrapper const ErrorBoundary: React.FC = (props) => { const { t } = useTranslation() const [errorBoundaryKey, setErrorBoundaryKey] = useState(0) const resetKeysRef = useRef(props.resetKeys) const prevResetKeysRef = useRef | undefined>(undefined) const copy = { componentStack: t('errorBoundary.componentStack', { ns: 'common' }), details: t('errorBoundary.details', { ns: 'common' }), error: `${t('error', { ns: 'common' })}:`, formatErrorCount: (count: number) => t('errorBoundary.errorCount', { ns: 'common', count }), message: t('errorBoundary.message', { ns: 'common' }), reload: t('errorBoundary.reloadPage', { ns: 'common' }), title: t('errorBoundary.title', { ns: 'common' }), tryAgain: t('errorBoundary.tryAgain', { ns: 'common' }), } const resetErrorBoundary = useCallback(() => { setErrorBoundaryKey(prev => prev + 1) props.onReset?.() }, [props]) const onResetKeysChange = useCallback((prevResetKeys?: Array) => { prevResetKeysRef.current = prevResetKeys }, []) useEffect(() => { if (prevResetKeysRef.current !== props.resetKeys) resetKeysRef.current = props.resetKeys }, [props.resetKeys]) return ( ) } // Hook for imperative error handling export function useErrorHandler() { const [error, setError] = useState(null) useEffect(() => { if (error) throw error }, [error]) return setError } // Hook for catching async errors export function useAsyncError() { const [, setError] = useState() return useCallback( (error: Error) => { setError(() => { throw error }) }, [setError], ) } // HOC for wrapping components with error boundary export function withErrorBoundary

( Component: React.ComponentType

, errorBoundaryProps?: Omit, ): React.ComponentType

{ const WrappedComponent = (props: P) => ( ) WrappedComponent.displayName = `withErrorBoundary(${Component.displayName || Component.name || 'Component'})` return WrappedComponent } // Simple error fallback component export const ErrorFallback: React.FC<{ error: Error resetErrorBoundaryAction: () => void }> = ({ error, resetErrorBoundaryAction }) => { const { t } = useTranslation() return (

{t('errorBoundary.fallbackTitle', { ns: 'common' })}

{error.message}

) } export default ErrorBoundary