Files
gridpilot.gg/apps/website/components/errors/ApiErrorBoundary.tsx
2026-01-19 18:01:30 +01:00

155 lines
3.8 KiB
TypeScript

'use client';
import React, { Component, ReactNode, useState } from 'react';
import { ApiError } from '@/lib/api/base/ApiError';
import { connectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
import { ErrorDisplay } from '@/components/shared/ErrorDisplay';
import { DevErrorPanel } from '@/components/shared/DevErrorPanel';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: ApiError) => void;
}
interface State {
hasError: boolean;
error: ApiError | null;
isDev: boolean;
}
/**
* Error Boundary for API-related errors
* Catches errors from API calls and displays appropriate UI
*/
export class ApiErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null,
isDev: process.env.NODE_ENV === 'development',
};
}
static getDerivedStateFromError(error: Error): State {
// Only handle ApiError instances
if (error instanceof ApiError) {
return {
hasError: true,
error,
isDev: process.env.NODE_ENV === 'development',
};
}
// Re-throw non-API errors
throw error;
}
componentDidCatch(error: Error): void {
if (error instanceof ApiError) {
// Report to connection monitor
connectionMonitor.recordFailure(error);
// Call custom error handler if provided
if (this.props.onError) {
this.props.onError(error);
}
// For connectivity errors in production, don't show error boundary UI
// These are handled by the notification system
if (error.isConnectivityIssue() && !this.state.isDev) {
// Reset error state so boundary doesn't block UI
setTimeout(() => this.resetError(), 100);
return;
}
}
}
componentDidMount(): void {
// Listen for connection status changes
const monitor = connectionMonitor;
monitor.on('disconnected', this.handleDisconnected);
monitor.on('degraded', this.handleDegraded);
monitor.on('connected', this.handleConnected);
}
componentWillUnmount(): void {
const monitor = connectionMonitor;
monitor.off('disconnected', this.handleDisconnected);
monitor.off('degraded', this.handleDegraded);
monitor.off('connected', this.handleConnected);
}
private handleDisconnected = (): void => {
// Connection status handled by notification system
};
private handleDegraded = (): void => {
// Connection status handled by notification system
};
private handleConnected = (): void => {
// Connection status handled by notification system
};
resetError = (): void => {
this.setState({ hasError: false, error: null });
};
render(): ReactNode {
if (this.state.hasError && this.state.error) {
if (this.props.fallback) {
return this.props.fallback;
}
// Show different UI based on environment
if (this.state.isDev) {
return (
<DevErrorPanel
error={this.state.error}
onReset={this.resetError}
/>
);
}
return (
<ErrorDisplay
error={this.state.error}
onRetry={this.resetError}
/>
);
}
return this.props.children;
}
}
/**
* Hook-based alternative for functional components
*/
export function useApiErrorBoundary() {
const [error, setError] = useState<ApiError | null>(null);
const [isDev] = useState(process.env.NODE_ENV === 'development');
const handleError = (err: ApiError) => {
setError(err);
};
const reset = () => {
setError(null);
};
return {
error,
isDev,
handleError,
reset,
ErrorBoundary: ({ children }: { children: ReactNode }) => (
<ApiErrorBoundary onError={handleError}>
{children}
</ApiErrorBoundary>
),
};
}