Files
fusion/packages/dashboard/app/components/ErrorBoundary.tsx
Fusion ca26ae0781 fix(dashboard): reload on stale chunk after redeploy
Builds now emit version.json + a __BUILD_VERSION__ define. The client
re-checks the remote version on visibilitychange/focus and reloads on
mismatch, so a backgrounded tab doesn't hit a 404'd hashed chunk and
surface "'text/html' is not a valid JavaScript MIME type" when opening
Settings. ErrorBoundary catches stale-chunk errors as a safety net.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 10:01:21 -07:00

98 lines
2.8 KiB
TypeScript

import { Component, type ReactNode, type ErrorInfo } from "react";
import { AlertTriangle } from "lucide-react";
import { handleChunkLoadError } from "../versionCheck";
import "./ErrorBoundary.css";
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
level?: "page" | "modal" | "root";
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.error("[ErrorBoundary]", error, errorInfo);
if (handleChunkLoadError(error)) return;
this.props.onError?.(error, errorInfo);
}
resetErrorBoundary = (): void => {
this.setState({ hasError: false, error: null });
};
render(): ReactNode {
if (!this.state.hasError) {
return this.props.children;
}
if (this.props.fallback) {
return this.props.fallback;
}
const level = this.props.level ?? "page";
const isModal = level === "modal";
const title = isModal
? "This section encountered an error"
: "Something went wrong";
return (
<div className={`error-boundary error-boundary--${level}`}>
<div className="error-boundary__icon">
<AlertTriangle size={40} />
</div>
<div className="error-boundary__title">{title}</div>
{this.state.error && (
<pre className="error-boundary__message">{this.state.error.message}</pre>
)}
<div className="error-boundary__actions">
<button className="btn btn-primary" onClick={this.resetErrorBoundary}>
Retry
</button>
<button className="btn" onClick={() => window.location.reload()}>
Reload page
</button>
</div>
</div>
);
}
}
export function PageErrorBoundary({ children, onError }: { children: ReactNode; onError?: (error: Error, errorInfo: ErrorInfo) => void }) {
return (
<ErrorBoundary level="page" onError={onError}>
{children}
</ErrorBoundary>
);
}
export function ModalErrorBoundary({ children, onError }: { children: ReactNode; onError?: (error: Error, errorInfo: ErrorInfo) => void }) {
return (
<ErrorBoundary level="modal" onError={onError}>
{children}
</ErrorBoundary>
);
}
export function RootErrorBoundary({ children, onError }: { children: ReactNode; onError?: (error: Error, errorInfo: ErrorInfo) => void }) {
return (
<ErrorBoundary level="root" onError={onError}>
{children}
</ErrorBoundary>
);
}