diff --git a/apps/timo-web/components/boundary/AsyncBoundary.tsx b/apps/timo-web/components/boundary/AsyncBoundary.tsx new file mode 100644 index 00000000..07eb3111 --- /dev/null +++ b/apps/timo-web/components/boundary/AsyncBoundary.tsx @@ -0,0 +1,26 @@ +import { Suspense, type ReactNode } from "react"; + +import { + ErrorBoundary, + type ErrorFallbackTypes, +} from "@/components/boundary/ErrorBoundary"; + +interface AsyncBoundaryProps { + children: ReactNode; + pendingFallback?: ReactNode; + errorFallback?: ErrorFallbackTypes; +} + +export const AsyncBoundary = ({ + children, + pendingFallback = null, + errorFallback, +}: AsyncBoundaryProps) => { + const content = {children}; + + if (!errorFallback) { + return content; + } + + return {content}; +}; diff --git a/apps/timo-web/components/boundary/ErrorBoundary.tsx b/apps/timo-web/components/boundary/ErrorBoundary.tsx new file mode 100644 index 00000000..edf80bd9 --- /dev/null +++ b/apps/timo-web/components/boundary/ErrorBoundary.tsx @@ -0,0 +1,56 @@ +"use client"; + +import * as Sentry from "@sentry/nextjs"; +import { Component } from "react"; + +import type { ErrorInfo, ReactNode } from "react"; + +export type ErrorFallbackTypes = + | ReactNode + | ((error: Error, reset: () => void) => ReactNode); + +interface ErrorBoundaryProps { + children: ReactNode; + fallback: ErrorFallbackTypes; +} + +interface ErrorBoundaryState { + error: Error | null; +} + +const toError = (value: unknown): Error => + value instanceof Error ? value : new Error(String(value)); + +export class ErrorBoundary extends Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + state: ErrorBoundaryState = { error: null }; + + static getDerivedStateFromError(error: unknown): ErrorBoundaryState { + return { error: toError(error) }; + } + + componentDidCatch(error: unknown, errorInfo: ErrorInfo): void { + Sentry.captureException(toError(error), { + contexts: { react: { componentStack: errorInfo.componentStack } }, + }); + } + + reset = (): void => { + this.setState({ error: null }); + }; + + render(): ReactNode { + const { error } = this.state; + const { children, fallback } = this.props; + + if (error) { + return typeof fallback === "function" + ? fallback(error, this.reset) + : fallback; + } + + return children; + } +}