Files
2024-02-15 17:01:50 +00:00

121 lines
3.0 KiB
TypeScript

import type {ErrorInfo} from 'react';
import {Component, lazy, Suspense, useMemo} from 'react';
import styled from '@emotion/styled';
import * as Sentry from '@sentry/react';
import LoadingError from 'sentry/components/loadingError';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import {t} from 'sentry/locale';
import {isWebpackChunkLoadingError} from 'sentry/utils';
import retryableImport from 'sentry/utils/retryableImport';
type PromisedImport<C> = Promise<{default: C}>;
type ComponentType = React.ComponentType<any>;
type Props<C extends ComponentType> = React.ComponentProps<C> & {
/**
* Accepts a function to trigger the import resolution of the component.
*/
component: () => PromisedImport<C>;
/**
* Override the default fallback component.
*
* Try not to load too many unique components for the fallback!
*/
loadingFallback?: React.ReactNode | undefined;
};
/**
* LazyLoad is used to dynamically load codesplit components via a `import`
* call. This is primarily used in our routing tree.
*
* <LazyLoad component={() => import('./myComponent')} someComponentProps={...} />
*/
function LazyLoad<C extends ComponentType>({
component,
loadingFallback,
...props
}: Props<C>) {
const LazyComponent = useMemo(
() => lazy<C>(() => retryableImport(component)),
[component]
);
return (
<ErrorBoundary>
<Suspense
fallback={
loadingFallback ?? (
<LoadingContainer>
<LoadingIndicator />
</LoadingContainer>
)
}
>
<LazyComponent {...(props as React.ComponentProps<C>)} />
</Suspense>
</ErrorBoundary>
);
}
interface ErrorBoundaryState {
error: Error | null;
hasError: boolean;
}
// Error boundaries currently have to be classes.
class ErrorBoundary extends Component<{}, ErrorBoundaryState> {
static getDerivedStateFromError(error: Error) {
return {
hasError: true,
error,
};
}
state = {hasError: false, error: null};
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
Sentry.withScope(scope => {
if (isWebpackChunkLoadingError(error)) {
scope.setFingerprint(['webpack', 'error loading chunk']);
}
scope.setExtra('errorInfo', errorInfo);
Sentry.captureException(error);
});
// eslint-disable-next-line no-console
console.error(error);
}
// Reset `hasError` so that we attempt to render `this.props.children` again
handleRetry = () => this.setState({hasError: false});
render() {
if (this.state.hasError) {
return (
<LoadingErrorContainer>
<LoadingError
onRetry={this.handleRetry}
message={t('There was an error loading a component.')}
/>
</LoadingErrorContainer>
);
}
return this.props.children;
}
}
const LoadingContainer = styled('div')`
display: flex;
flex: 1;
align-items: center;
`;
const LoadingErrorContainer = styled('div')`
flex: 1;
`;
export default LazyLoad;