lazyLoad.tsx 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import {Component, ErrorInfo, lazy, Suspense, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import LoadingError from 'sentry/components/loadingError';
  5. import LoadingIndicator from 'sentry/components/loadingIndicator';
  6. import {t} from 'sentry/locale';
  7. import {isWebpackChunkLoadingError} from 'sentry/utils';
  8. import retryableImport from 'sentry/utils/retryableImport';
  9. type PromisedImport<C> = Promise<{default: C}>;
  10. type ComponentType = React.ComponentType<any>;
  11. type Props<C extends ComponentType> = Omit<React.ComponentProps<C>, 'route'> & {
  12. /**
  13. * Accepts a function to trigger the import resolution of the component.
  14. */
  15. component: () => PromisedImport<C>;
  16. };
  17. /**
  18. * LazyLoad is used to dynamically load codesplit components via a `import`
  19. * call. This is primarily used in our routing tree.
  20. *
  21. * <LazyLoad component={() => import('./myComponent')} someComponentProps={...} />
  22. */
  23. function LazyLoad<C extends ComponentType>({component, ...props}: Props<C>) {
  24. const LazyComponent = useMemo(
  25. () => lazy(() => retryableImport(component)),
  26. [component]
  27. );
  28. return (
  29. <ErrorBoundary>
  30. <Suspense
  31. fallback={
  32. <LoadingContainer>
  33. <LoadingIndicator />
  34. </LoadingContainer>
  35. }
  36. >
  37. <LazyComponent {...(props as React.ComponentProps<C>)} />
  38. </Suspense>
  39. </ErrorBoundary>
  40. );
  41. }
  42. interface ErrorBoundaryState {
  43. error: Error | null;
  44. hasError: boolean;
  45. }
  46. // Error boundaries currently have to be classes.
  47. class ErrorBoundary extends Component<{}, ErrorBoundaryState> {
  48. static getDerivedStateFromError(error: Error) {
  49. return {
  50. hasError: true,
  51. error,
  52. };
  53. }
  54. state = {hasError: false, error: null};
  55. componentDidCatch(error: Error, errorInfo: ErrorInfo) {
  56. Sentry.withScope(scope => {
  57. if (isWebpackChunkLoadingError(error)) {
  58. scope.setFingerprint(['webpack', 'error loading chunk']);
  59. }
  60. scope.setExtra('errorInfo', errorInfo);
  61. Sentry.captureException(error);
  62. });
  63. // eslint-disable-next-line no-console
  64. console.error(error);
  65. }
  66. // Reset `hasError` so that we attempt to render `this.props.children` again
  67. handleRetry = () => this.setState({hasError: false});
  68. render() {
  69. if (this.state.hasError) {
  70. return (
  71. <LoadingErrorContainer>
  72. <LoadingError
  73. onRetry={this.handleRetry}
  74. message={t('There was an error loading a component.')}
  75. />
  76. </LoadingErrorContainer>
  77. );
  78. }
  79. return this.props.children;
  80. }
  81. }
  82. const LoadingContainer = styled('div')`
  83. display: flex;
  84. flex: 1;
  85. align-items: center;
  86. `;
  87. const LoadingErrorContainer = styled('div')`
  88. flex: 1;
  89. `;
  90. export default LazyLoad;