error-boundary.tsx 1.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
  1. import { Component, Fragment, type ErrorInfo, type ReactNode } from 'react';
  2. import { captureError } from './health';
  3. type Props = {
  4. children?: ReactNode;
  5. /** What shows instead of the children; `reset` renders them again from scratch. */
  6. fallback: (error: Error, reset: () => void) => ReactNode;
  7. /** The app can't go on without this part (the root): reported as fatal. */
  8. fatal?: boolean;
  9. /** A new value (the next page) clears the error. */
  10. resetKey?: unknown;
  11. };
  12. type State = { error: Error | null; resetKey: unknown; generation: number };
  13. /**
  14. * Catches render errors, reports them to UNA's analytics (`react` errors with the component
  15. * stack) and shows `fallback`. React Native needs boundaries for this: render errors never
  16. * reach its global error handler. Next's `error.tsx` and `global-error.tsx` play this part
  17. * on web.
  18. */
  19. export class AnalyticsErrorBoundary extends Component<Props, State> {
  20. constructor(props: Props) {
  21. super(props);
  22. this.state = { error: null, resetKey: props.resetKey, generation: 0 };
  23. }
  24. static getDerivedStateFromError(error: unknown): Partial<State> {
  25. return { error: error instanceof Error ? error : new Error(String(error)) };
  26. }
  27. static getDerivedStateFromProps(props: Props, state: State): Partial<State> | null {
  28. return Object.is(props.resetKey, state.resetKey) ? null : { resetKey: props.resetKey, error: null };
  29. }
  30. componentDidCatch(error: unknown, info: ErrorInfo): void {
  31. captureError('react', { error, componentStack: info.componentStack ?? '', fatal: !!this.props.fatal });
  32. }
  33. reset = (): void => {
  34. this.setState((state) => ({ error: null, generation: state.generation + 1 }));
  35. };
  36. render(): ReactNode {
  37. if (this.state.error) return this.props.fallback(this.state.error, this.reset);
  38. return <Fragment key={this.state.generation}>{this.props.children}</Fragment>;
  39. }
  40. }