lazyLoad.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import React from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import LoadingError from 'app/components/loadingError';
  5. import LoadingIndicator from 'app/components/loadingIndicator';
  6. import {t} from 'app/locale';
  7. import {isWebpackChunkLoadingError} from 'app/utils';
  8. import retryableImport from 'app/utils/retryableImport';
  9. type PromisedImport<C> = Promise<{default: C}>;
  10. type Component = React.ComponentType<any>;
  11. type Props<C extends Component> = Omit<
  12. React.ComponentProps<C>,
  13. 'hideBusy' | 'hideError' | 'component' | 'route'
  14. > & {
  15. hideBusy?: boolean;
  16. hideError?: boolean;
  17. /**
  18. * Function that returns a promise of a React.Component
  19. */
  20. component?: () => PromisedImport<C>;
  21. /**
  22. * Also accepts a route object from react-router that has a `componentPromise` property
  23. */
  24. route?: {componentPromise: () => PromisedImport<C>};
  25. };
  26. type State<C extends Component> = {
  27. Component: C | null;
  28. error: any | null;
  29. };
  30. class LazyLoad<C extends Component> extends React.Component<Props<C>, State<C>> {
  31. state: State<C> = {
  32. Component: null,
  33. error: null,
  34. };
  35. componentDidMount() {
  36. this.fetchComponent();
  37. }
  38. UNSAFE_componentWillReceiveProps(nextProps: Props<C>) {
  39. // No need to refetch when component does not change
  40. if (nextProps.component && nextProps.component === this.props.component) {
  41. return;
  42. }
  43. // This is to handle the following case:
  44. // <Route path="a/">
  45. // <Route path="b/" component={LazyLoad} componentPromise={...} />
  46. // <Route path="c/" component={LazyLoad} componentPromise={...} />
  47. // </Route>
  48. //
  49. // `LazyLoad` will get not fully remount when we switch between `b` and `c`,
  50. // instead will just re-render. Refetch if route paths are different
  51. if (nextProps.route && nextProps.route === this.props.route) {
  52. return;
  53. }
  54. // If `this.fetchComponent` is not in callback,
  55. // then there's no guarantee that new Component will be rendered
  56. this.setState(
  57. {
  58. Component: null,
  59. },
  60. this.fetchComponent
  61. );
  62. }
  63. componentDidCatch(error: any) {
  64. Sentry.captureException(error);
  65. this.handleError(error);
  66. }
  67. get componentGetter() {
  68. return this.props.component ?? this.props.route?.componentPromise;
  69. }
  70. handleFetchError = (error: any) => {
  71. Sentry.withScope(scope => {
  72. if (isWebpackChunkLoadingError(error)) {
  73. scope.setFingerprint(['webpack', 'error loading chunk']);
  74. }
  75. Sentry.captureException(error);
  76. });
  77. this.handleError(error);
  78. };
  79. handleError = (error: any) => {
  80. // eslint-disable-next-line no-console
  81. console.error(error);
  82. this.setState({error});
  83. };
  84. fetchComponent = async () => {
  85. const getComponent = this.componentGetter;
  86. if (getComponent === undefined) {
  87. return;
  88. }
  89. try {
  90. this.setState({Component: await retryableImport(getComponent)});
  91. } catch (err) {
  92. this.handleFetchError(err);
  93. }
  94. };
  95. fetchRetry = () => {
  96. this.setState({error: null}, this.fetchComponent);
  97. };
  98. render() {
  99. const {Component, error} = this.state;
  100. const {hideBusy, hideError, component: _component, ...otherProps} = this.props;
  101. if (error && !hideError) {
  102. return (
  103. <LoadingErrorContainer>
  104. <LoadingError
  105. onRetry={this.fetchRetry}
  106. message={t('There was an error loading a component.')}
  107. />
  108. </LoadingErrorContainer>
  109. );
  110. }
  111. if (!Component && !hideBusy) {
  112. return (
  113. <LoadingContainer>
  114. <LoadingIndicator />
  115. </LoadingContainer>
  116. );
  117. }
  118. if (Component === null) {
  119. return null;
  120. }
  121. return <Component {...(otherProps as React.ComponentProps<C>)} />;
  122. }
  123. }
  124. const LoadingContainer = styled('div')`
  125. display: flex;
  126. flex: 1;
  127. align-items: center;
  128. `;
  129. const LoadingErrorContainer = styled('div')`
  130. flex: 1;
  131. `;
  132. export default LazyLoad;