ErrorBoundary.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import { Component, type ReactNode, type ErrorInfo } from "react"
  2. interface ErrorBoundaryProps {
  3. children: ReactNode
  4. fallback?: (error: Error, errorInfo: ErrorInfo, reset: () => void) => ReactNode
  5. onError?: (error: Error, errorInfo: ErrorInfo) => void
  6. }
  7. interface ErrorBoundaryState {
  8. hasError: boolean
  9. error: Error | null
  10. errorInfo: ErrorInfo | null
  11. }
  12. /**
  13. * Error boundary component to catch and handle React errors
  14. *
  15. * Usage:
  16. * ```tsx
  17. * <ErrorBoundary>
  18. * <App />
  19. * </ErrorBoundary>
  20. * ```
  21. *
  22. * With custom fallback:
  23. * ```tsx
  24. * <ErrorBoundary fallback={(error, errorInfo, reset) => (
  25. * <div>
  26. * <h1>Error: {error.message}</h1>
  27. * <button onClick={reset}>Retry</button>
  28. * </div>
  29. * )}>
  30. * <App />
  31. * </ErrorBoundary>
  32. * ```
  33. */
  34. export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  35. constructor(props: ErrorBoundaryProps) {
  36. super(props)
  37. this.state = {
  38. hasError: false,
  39. error: null,
  40. errorInfo: null,
  41. }
  42. }
  43. static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
  44. return {
  45. hasError: true,
  46. error,
  47. }
  48. }
  49. override componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
  50. console.error("[ErrorBoundary] Caught error:", error, errorInfo)
  51. this.setState({
  52. errorInfo,
  53. })
  54. // Call optional error handler for logging/telemetry
  55. if (this.props.onError) {
  56. this.props.onError(error, errorInfo)
  57. }
  58. }
  59. reset = (): void => {
  60. this.setState({
  61. hasError: false,
  62. error: null,
  63. errorInfo: null,
  64. })
  65. }
  66. override render(): ReactNode {
  67. if (this.state.hasError && this.state.error) {
  68. // Custom fallback if provided
  69. if (this.props.fallback) {
  70. const errorInfo = this.state.errorInfo ?? ({ componentStack: "" } as ErrorInfo)
  71. return this.props.fallback(this.state.error, errorInfo, this.reset)
  72. }
  73. // Default fallback UI
  74. return (
  75. <div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center p-4">
  76. <div className="max-w-2xl w-full bg-white dark:bg-gray-950 border border-red-300 dark:border-red-700 rounded-lg shadow-lg p-6">
  77. <div className="flex items-start gap-4">
  78. {/* Error icon */}
  79. <div className="flex-shrink-0">
  80. <svg
  81. className="w-8 h-8 text-red-600 dark:text-red-400"
  82. fill="none"
  83. stroke="currentColor"
  84. viewBox="0 0 24 24"
  85. >
  86. <path
  87. strokeLinecap="round"
  88. strokeLinejoin="round"
  89. strokeWidth={2}
  90. d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
  91. />
  92. </svg>
  93. </div>
  94. {/* Error content */}
  95. <div className="flex-1 min-w-0">
  96. <h1 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-2">Something went wrong</h1>
  97. <p className="text-sm text-gray-700 dark:text-gray-300 mb-4">
  98. An unexpected error occurred in the application.
  99. </p>
  100. {/* Error details */}
  101. <div className="bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded p-3 mb-4">
  102. <h2 className="text-sm font-semibold text-red-900 dark:text-red-100 mb-1">
  103. Error: {this.state.error.message}
  104. </h2>
  105. {this.state.error.stack && (
  106. <pre className="text-xs text-red-700 dark:text-red-300 overflow-x-auto whitespace-pre-wrap">
  107. {this.state.error.stack}
  108. </pre>
  109. )}
  110. </div>
  111. {/* Component stack (in development) */}
  112. {this.state.errorInfo?.componentStack && import.meta.env.DEV && (
  113. <details className="mb-4">
  114. <summary className="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer hover:text-gray-900 dark:hover:text-gray-100">
  115. Component Stack
  116. </summary>
  117. <pre className="mt-2 text-xs text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-800 p-2 rounded overflow-x-auto whitespace-pre-wrap">
  118. {this.state.errorInfo.componentStack}
  119. </pre>
  120. </details>
  121. )}
  122. {/* Actions */}
  123. <div className="flex gap-2">
  124. <button
  125. onClick={this.reset}
  126. className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded transition-colors"
  127. >
  128. Try Again
  129. </button>
  130. <button
  131. onClick={() => window.location.reload()}
  132. className="px-4 py-2 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100 text-sm font-medium rounded transition-colors"
  133. >
  134. Reload Page
  135. </button>
  136. </div>
  137. </div>
  138. </div>
  139. </div>
  140. </div>
  141. )
  142. }
  143. return this.props.children
  144. }
  145. }