PrithviGuardian / client /src /components /ErrorBoundary.tsx
Varad Bakshi
Fresh upload
e7427b5
Raw
History Blame Contribute Delete
10.3 kB
// --- File: client/src/components/ErrorBoundary.tsx ---
// --- Tech Used ---
// - React: For building the user interface. Uses both Class Components (for ErrorBoundary itself) and Functional Components (for hooks/HOCs).
// - TypeScript: For static typing, improving code quality and maintainability.
// - Shadcn/ui (Likely): Based on "@/components/ui/..." imports. This library provides accessible and customizable UI components, often built on Tailwind CSS and Radix UI.
// - Lucide Icons (`lucide-react`): For clean and consistent SVG icons.
// - Custom Error Handling (`@/lib/errorHandler`): A centralized module for logging/processing errors.
// --- Imports ---
import React, { Component, ReactNode, ErrorInfo } from 'react'; // React.ErrorInfo provides types for componentDidCatch
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; // Added CardFooter
import { AlertTriangle, RefreshCw, Home, Copy } from "lucide-react"; // Added Copy icon
import { errorHandler } from "@/lib/errorHandler"; // Your custom error logging utility
// --- Component Props Interface ---
// Defines the expected properties for the ErrorBoundary component.
interface Props {
children: ReactNode; // The React elements that this boundary will wrap.
fallback?: ReactNode; // Optional custom UI to render when an error is caught.
moduleName?: string; // A more descriptive name for the module/component being wrapped.
onRetry?: () => void; // Optional callback for when the user clicks "Try Again".
}
// --- Component State Interface ---
// Defines the structure of the ErrorBoundary's internal state.
interface State {
hasError: boolean; // True if an error has been caught.
error?: Error; // The caught JavaScript error object.
errorInfo?: ErrorInfo; // Additional info from React about the component stack.
errorId?: string; // An ID for the logged error, useful for tracking.
}
// --- ErrorBoundary Class Component ---
// Function: Catches JavaScript errors in its child component tree, logs them, and displays a fallback UI.
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
// Initialize state: no error by default.
this.state = { hasError: false };
}
// Lifecycle method: Called during the "render" phase if an error is thrown by a descendant.
// Updates the state to trigger a re-render with the fallback UI.
static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error };
}
// Lifecycle method: Called during the "commit" phase after an error is thrown and getDerivedStateFromError has been called.
// Suitable for side effects like logging.
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
const { moduleName } = this.props;
const errorContext = moduleName || 'UnknownComponent';
// Log the error using the custom error handler.
const errorId = errorHandler.logError(
error,
errorContext,
'react_component_crash', // A category for this type of error
{ componentStack: errorInfo.componentStack } // Include component stack
);
// Update state with the error information and ID.
this.setState({ errorInfo, errorId });
// Log to console for development purposes.
console.error(`ErrorBoundary (${errorContext}): Uncaught error:`, error, errorInfo);
}
// Resets the error state, allowing children to attempt re-rendering.
handleRetry = () => {
if (this.props.onRetry) {
this.props.onRetry(); // Call custom retry logic if provided
}
this.setState({ hasError: false, error: undefined, errorInfo: undefined, errorId: undefined });
};
// Navigates to the home page.
// For SPAs, using a router's navigation method (e.g., useNavigate from react-router-dom) is better than a hard reload.
handleGoHome = () => {
// TODO: Replace with router navigation if using react-router or similar.
// Example with react-router: navigate('/');
window.location.href = '/';
};
// Copies the error ID to the clipboard.
handleCopyErrorId = () => {
if (this.state.errorId) {
navigator.clipboard.writeText(this.state.errorId)
.then(() => {
// Optional: Show a temporary "Copied!" message
console.info("Error ID copied to clipboard:", this.state.errorId);
})
.catch(err => {
console.error("Failed to copy error ID:", err);
});
}
};
render() {
if (this.state.hasError) {
// Render custom fallback if provided
if (this.props.fallback) {
return this.props.fallback;
}
// Default fallback UI
const { error, errorId, errorInfo } = this.state;
const { moduleName } = this.props;
const componentName = moduleName || 'the application';
return (
<div role="alert" className="min-h-screen bg-background text-foreground flex items-center justify-center p-4">
<Card className="max-w-lg w-full shadow-xl border-destructive">
<CardHeader className="text-center bg-destructive/10">
<div className="mx-auto w-16 h-16 bg-destructive/20 rounded-full flex items-center justify-center mb-4 ring-4 ring-destructive/30">
<AlertTriangle className="w-8 h-8 text-destructive" />
</div>
<CardTitle className="text-2xl font-bold text-destructive">An Error Occurred</CardTitle>
<CardDescription className="text-muted-foreground">
Sorry, something went wrong in {componentName}.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4 pt-6">
<div className="bg-muted p-4 rounded-md border text-sm">
<strong className="text-card-foreground">Message:</strong>
<p className="font-mono text-destructive break-words whitespace-pre-wrap">
{error?.message || 'No specific error message available.'}
</p>
{errorId && (
<div className="mt-3 text-xs text-muted-foreground flex items-center justify-between">
<span>Error ID: {errorId}</span>
<Button variant="ghost" size="sm" onClick={this.handleCopyErrorId} aria-label="Copy Error ID">
<Copy className="w-3 h-3 mr-1" /> Copy ID
</Button>
</div>
)}
</div>
{errorInfo?.componentStack && process.env.NODE_ENV === 'development' && (
<details className="text-xs bg-muted p-2 rounded">
<summary className="cursor-pointer text-muted-foreground">Component Stack (Dev Only)</summary>
<pre className="mt-1 whitespace-pre-wrap break-all text-card-foreground/80">
{errorInfo.componentStack}
</pre>
</details>
)}
</CardContent>
<CardFooter className="flex flex-col sm:flex-row sm:justify-end gap-3 pt-6 border-t">
<Button onClick={this.handleGoHome} variant="outline" className="w-full sm:w-auto">
<Home className="w-4 h-4 mr-2" /> Go to Dashboard
</Button>
<Button onClick={this.handleRetry} variant="default" className="w-full sm:w-auto">
<RefreshCw className="w-4 h-4 mr-2" /> Try Again
</Button>
</CardFooter>
</Card>
</div>
);
}
return this.props.children; // Render children if no error
}
}
// --- Custom Hook: useErrorHandler ---
// Function: Provides convenient error handling functions for use in Functional Components.
export function useErrorHandler(moduleName: string) { // moduleName: Context for the error (e.g., component name).
// Function: Manually logs an error using the centralized errorHandler.
const handleError = (error: Error, userActionContext?: string): string | undefined => {
console.error(`Error handled in module '${moduleName}' (action: ${userActionContext || 'N/A'}):`, error);
return errorHandler.logError(error, moduleName, userActionContext);
};
// Function: Wraps an asynchronous operation (Promise) to automatically catch and log errors.
// T: Generic type for the expected data from the successful operation.
// SIMPLIFIED VERSION FOR TESTING
const handleAsyncOperation = async ( // Generic <T> removed for simplicity during testing
operation: any, // Using 'any' for the operation itself
userActionContext?: string | undefined // Adding userActionContext back as an optional string
): Promise<any> => { // Using 'any' for the Promise return type
try {
const data = await operation();
return { success: true, data };
} catch (err) {
// Ensure the caught item is an Error instance before logging.
const error = err instanceof Error ? err : new Error(String(err));
const errorId = handleError(error, userActionContext);
return { success: false, errorId };
}
};
return { handleError, handleAsyncOperation };
}
// --- Higher-Order Component (HOC): withErrorBoundary ---
// Function: A utility to easily wrap existing components with the ErrorBoundary.
// P: Generic type for the props of the component being wrapped.
export function withErrorBoundary<P extends React.JSX.IntrinsicAttributes>( // Use a more appropriate constraint for P
WrappedComponent: React.ComponentType<P>,
options: { moduleName: string; fallback?: ReactNode; onRetry?: () => void } // Options object
) {
const { moduleName, fallback, onRetry } = options;
// Returns a new component that renders the WrappedComponent inside an ErrorBoundary.
const ComponentWithBoundary = (props: P) => ( // Give component a display name
<ErrorBoundary moduleName={moduleName} fallback={fallback} onRetry={onRetry}>
<WrappedComponent {...props} />
</ErrorBoundary>
);
// Set a display name for easier debugging in React DevTools
const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
ComponentWithBoundary.displayName = `WithErrorBoundary(${wrappedComponentName})`;
return ComponentWithBoundary;
}