54 lines
1.7 KiB
TypeScript
54 lines
1.7 KiB
TypeScript
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
|
|
|
interface Props {
|
|
children: ReactNode;
|
|
/** Optional fallback rendered in place of `children` when an error is caught. */
|
|
fallback?: (error: Error) => ReactNode;
|
|
}
|
|
|
|
interface State {
|
|
error: Error | null;
|
|
}
|
|
|
|
/**
|
|
* Catches render errors in its subtree and renders a fallback instead of
|
|
* letting them crash the whole page. Used to isolate failures in part viewers
|
|
* (e.g. WebGL context loss, texture/trace load errors).
|
|
*/
|
|
export default class ErrorBoundary extends Component<Props, State> {
|
|
override state: State = { error: null };
|
|
|
|
static getDerivedStateFromError(error: Error): State {
|
|
return { error };
|
|
}
|
|
|
|
override componentDidCatch(error: Error, info: ErrorInfo) {
|
|
// Surface the error for debugging without breaking the UI.
|
|
console.error('Part viewer error:', error, info.componentStack);
|
|
}
|
|
|
|
override render() {
|
|
if (this.state.error) {
|
|
return this.props.fallback
|
|
? this.props.fallback(this.state.error)
|
|
: <DefaultFallback error={this.state.error} />;
|
|
}
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
function DefaultFallback({ error }: { error: Error }) {
|
|
return (
|
|
<div className="flex h-80 flex-col items-center justify-center gap-2 overflow-auto rounded-lg border border-zinc-800 bg-zinc-900 p-4 text-center">
|
|
<p className="text-sm font-medium text-zinc-200">Couldn't render this part</p>
|
|
<p className="max-w-sm wrap-break-word font-mono text-xs text-zinc-400">
|
|
{error.message}
|
|
</p>
|
|
{error.stack && (
|
|
<pre className="mt-2 max-h-40 w-full overflow-auto whitespace-pre-wrap text-left font-mono text-[10px] leading-relaxed text-zinc-600">
|
|
{error.stack}
|
|
</pre>
|
|
)}
|
|
</div>
|
|
);
|
|
} |