import React from 'react'; import styled from '@emotion/styled'; import * as Sentry from '@sentry/react'; import LoadingError from 'app/components/loadingError'; import LoadingIndicator from 'app/components/loadingIndicator'; import {t} from 'app/locale'; import {isWebpackChunkLoadingError} from 'app/utils'; import retryableImport from 'app/utils/retryableImport'; type PromisedImport = Promise<{default: C}>; type Component = React.ComponentType; type Props = Omit< React.ComponentProps, 'hideBusy' | 'hideError' | 'component' | 'route' > & { hideBusy?: boolean; hideError?: boolean; /** * Function that returns a promise of a React.Component */ component?: () => PromisedImport; /** * Also accepts a route object from react-router that has a `componentPromise` property */ route?: {componentPromise: () => PromisedImport}; }; type State = { Component: C | null; error: any | null; }; class LazyLoad extends React.Component, State> { state: State = { Component: null, error: null, }; componentDidMount() { this.fetchComponent(); } UNSAFE_componentWillReceiveProps(nextProps: Props) { // No need to refetch when component does not change if (nextProps.component && nextProps.component === this.props.component) { return; } // This is to handle the following case: // // // // // // `LazyLoad` will get not fully remount when we switch between `b` and `c`, // instead will just re-render. Refetch if route paths are different if (nextProps.route && nextProps.route === this.props.route) { return; } // If `this.fetchComponent` is not in callback, // then there's no guarantee that new Component will be rendered this.setState( { Component: null, }, this.fetchComponent ); } componentDidCatch(error: any) { Sentry.captureException(error); this.handleError(error); } get componentGetter() { return this.props.component ?? this.props.route?.componentPromise; } handleFetchError = (error: any) => { Sentry.withScope(scope => { if (isWebpackChunkLoadingError(error)) { scope.setFingerprint(['webpack', 'error loading chunk']); } Sentry.captureException(error); }); this.handleError(error); }; handleError = (error: any) => { // eslint-disable-next-line no-console console.error(error); this.setState({error}); }; fetchComponent = async () => { const getComponent = this.componentGetter; if (getComponent === undefined) { return; } try { this.setState({Component: await retryableImport(getComponent)}); } catch (err) { this.handleFetchError(err); } }; fetchRetry = () => { this.setState({error: null}, this.fetchComponent); }; render() { const {Component, error} = this.state; const {hideBusy, hideError, component: _component, ...otherProps} = this.props; if (error && !hideError) { return ( ); } if (!Component && !hideBusy) { return ( ); } if (Component === null) { return null; } return )} />; } } const LoadingContainer = styled('div')` display: flex; flex: 1; align-items: center; `; const LoadingErrorContainer = styled('div')` flex: 1; `; export default LazyLoad;