Rewrite ErrorBoundary as a functional component using react-error-boundary
This commit is contained in:
parent
c5d527a667
commit
d7ea38cf22
4 changed files with 154 additions and 189 deletions
|
@ -134,6 +134,7 @@
|
|||
"react-color": "^2.19.3",
|
||||
"react-datepicker": "^4.8.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-error-boundary": "^4.0.11",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-hotkeys": "^1.1.4",
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import React, { ErrorInfo, useRef, useState } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import * as BuildConfig from 'soapbox/build-config';
|
||||
import { NODE_ENV } from 'soapbox/build-config';
|
||||
import { HStack, Text, Stack } from 'soapbox/components/ui';
|
||||
import { useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { captureSentryException } from 'soapbox/sentry';
|
||||
import KVStore from 'soapbox/storage/kv-store';
|
||||
import sourceCode from 'soapbox/utils/code';
|
||||
|
@ -12,72 +12,23 @@ import { unregisterSW } from 'soapbox/utils/sw';
|
|||
|
||||
import SiteLogo from './site-logo';
|
||||
|
||||
import type { RootState } from 'soapbox/store';
|
||||
|
||||
interface Props extends ReturnType<typeof mapStateToProps> {
|
||||
interface ISiteErrorBoundary {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
type State = {
|
||||
hasError: boolean;
|
||||
error: any;
|
||||
componentStack: any;
|
||||
browser?: Bowser.Parser.Parser;
|
||||
}
|
||||
/** Application-level error boundary. Fills the whole screen. */
|
||||
const SiteErrorBoundary: React.FC<ISiteErrorBoundary> = ({ children }) => {
|
||||
const { links } = useSoapboxConfig();
|
||||
const textarea = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||
const [error, setError] = useState<any>();
|
||||
const [componentStack, setComponentStack] = useState<any>();
|
||||
const [browser, setBrowser] = useState<Bowser.Parser.Parser>();
|
||||
|
||||
state: State = {
|
||||
hasError: false,
|
||||
error: undefined,
|
||||
componentStack: undefined,
|
||||
browser: undefined,
|
||||
};
|
||||
const isProduction = NODE_ENV === 'production';
|
||||
const errorText = error + componentStack;
|
||||
|
||||
textarea: HTMLTextAreaElement | null = null;
|
||||
|
||||
componentDidCatch(error: any, info: any): void {
|
||||
captureSentryException(error, {
|
||||
tags: {
|
||||
// Allow page crashes to be easily searched in Sentry.
|
||||
ErrorBoundary: 'yes',
|
||||
},
|
||||
});
|
||||
|
||||
this.setState({
|
||||
hasError: true,
|
||||
error,
|
||||
componentStack: info && info.componentStack,
|
||||
});
|
||||
|
||||
import('bowser')
|
||||
.then(({ default: Bowser }) => {
|
||||
this.setState({
|
||||
browser: Bowser.getParser(window.navigator.userAgent),
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
setTextareaRef: React.RefCallback<HTMLTextAreaElement> = c => {
|
||||
this.textarea = c;
|
||||
};
|
||||
|
||||
handleCopy: React.MouseEventHandler = () => {
|
||||
if (!this.textarea) return;
|
||||
|
||||
this.textarea.select();
|
||||
this.textarea.setSelectionRange(0, 99999);
|
||||
|
||||
document.execCommand('copy');
|
||||
};
|
||||
|
||||
getErrorText = (): string => {
|
||||
const { error, componentStack } = this.state;
|
||||
return error + componentStack;
|
||||
};
|
||||
|
||||
clearCookies: React.MouseEventHandler = (e) => {
|
||||
const clearCookies: React.MouseEventHandler = (e) => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
KVStore.clear();
|
||||
|
@ -88,135 +39,142 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { browser, hasError } = this.state;
|
||||
const { children, links } = this.props;
|
||||
const handleCopy: React.MouseEventHandler = () => {
|
||||
if (!textarea.current) return;
|
||||
|
||||
if (!hasError) {
|
||||
return children;
|
||||
}
|
||||
textarea.current.select();
|
||||
textarea.current.setSelectionRange(0, 99999);
|
||||
|
||||
const isProduction = BuildConfig.NODE_ENV === 'production';
|
||||
document.execCommand('copy');
|
||||
};
|
||||
|
||||
const errorText = this.getErrorText();
|
||||
function handleError(error: Error, info: ErrorInfo) {
|
||||
setError(error);
|
||||
setComponentStack(info.componentStack);
|
||||
|
||||
return (
|
||||
<div className='flex h-screen flex-col bg-white pb-12 pt-16 dark:bg-primary-900'>
|
||||
<main className='mx-auto flex w-full max-w-7xl grow flex-col justify-center px-4 sm:px-6 lg:px-8'>
|
||||
<div className='flex shrink-0 justify-center'>
|
||||
<a href='/' className='inline-flex'>
|
||||
<SiteLogo alt='Logo' className='h-12 w-auto cursor-pointer' />
|
||||
</a>
|
||||
</div>
|
||||
captureSentryException(error, {
|
||||
tags: {
|
||||
// Allow page crashes to be easily searched in Sentry.
|
||||
ErrorBoundary: 'yes',
|
||||
},
|
||||
});
|
||||
|
||||
<div className='py-8'>
|
||||
<div className='mx-auto max-w-xl space-y-2 text-center'>
|
||||
<h1 className='text-3xl font-extrabold tracking-tight text-gray-900 dark:text-gray-500 sm:text-4xl'>
|
||||
<FormattedMessage id='alert.unexpected.message' defaultMessage='Something went wrong.' />
|
||||
</h1>
|
||||
<p className='text-lg text-gray-700 dark:text-gray-600'>
|
||||
<FormattedMessage
|
||||
id='alert.unexpected.body'
|
||||
defaultMessage="We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out)."
|
||||
values={{
|
||||
clearCookies: (
|
||||
<a href='/' onClick={this.clearCookies} className='text-primary-600 hover:underline dark:text-accent-blue'>
|
||||
<FormattedMessage
|
||||
id='alert.unexpected.clear_cookies'
|
||||
defaultMessage='clear cookies and browser data'
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<Text theme='muted'>
|
||||
<Text weight='medium' tag='span' theme='muted'>{sourceCode.displayName}:</Text>
|
||||
|
||||
{' '}{sourceCode.version}
|
||||
</Text>
|
||||
|
||||
<div className='mt-10'>
|
||||
<a href='/' className='text-base font-medium text-primary-600 hover:underline dark:text-accent-blue'>
|
||||
<FormattedMessage id='alert.unexpected.return_home' defaultMessage='Return Home' />
|
||||
{' '}
|
||||
<span className='inline-block rtl:rotate-180' aria-hidden='true'>→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isProduction && (
|
||||
<div className='mx-auto max-w-lg space-y-4 py-16'>
|
||||
{errorText && (
|
||||
<textarea
|
||||
ref={this.setTextareaRef}
|
||||
className='block h-48 w-full rounded-md border-gray-300 bg-gray-100 p-4 font-mono text-gray-900 shadow-sm focus:border-primary-500 focus:ring-2 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 sm:text-sm'
|
||||
value={errorText}
|
||||
onClick={this.handleCopy}
|
||||
dir='ltr'
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
|
||||
{browser && (
|
||||
<Stack>
|
||||
<Text weight='semibold'><FormattedMessage id='alert.unexpected.browser' defaultMessage='Browser' /></Text>
|
||||
<Text theme='muted'>{browser.getBrowserName()} {browser.getBrowserVersion()}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className='mx-auto w-full max-w-7xl shrink-0 px-4 sm:px-6 lg:px-8'>
|
||||
<HStack justifyContent='center' space={4} element='nav'>
|
||||
{links.get('status') && (
|
||||
<>
|
||||
<a href={links.get('status')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||
<FormattedMessage id='alert.unexpected.links.status' defaultMessage='Status' />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
||||
{links.get('help') && (
|
||||
<>
|
||||
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
||||
<a href={links.get('help')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||
<FormattedMessage id='alert.unexpected.links.help' defaultMessage='Help Center' />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
||||
{links.get('support') && (
|
||||
<>
|
||||
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
||||
<a href={links.get('support')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||
<FormattedMessage id='alert.unexpected.links.support' defaultMessage='Support' />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
import('bowser')
|
||||
.then(({ default: Bowser }) => setBrowser(Bowser.getParser(window.navigator.userAgent)))
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
}
|
||||
function goHome() {
|
||||
location.href = '/';
|
||||
}
|
||||
|
||||
function goHome() {
|
||||
location.href = '/';
|
||||
}
|
||||
const fallback = (
|
||||
<div className='flex h-screen flex-col bg-white pb-12 pt-16 dark:bg-primary-900'>
|
||||
<main className='mx-auto flex w-full max-w-7xl grow flex-col justify-center px-4 sm:px-6 lg:px-8'>
|
||||
<div className='flex shrink-0 justify-center'>
|
||||
<a href='/' className='inline-flex'>
|
||||
<SiteLogo alt='Logo' className='h-12 w-auto cursor-pointer' />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
function mapStateToProps(state: RootState) {
|
||||
const { links, logo } = getSoapboxConfig(state);
|
||||
<div className='py-8'>
|
||||
<div className='mx-auto max-w-xl space-y-2 text-center'>
|
||||
<h1 className='text-3xl font-extrabold tracking-tight text-gray-900 dark:text-gray-500 sm:text-4xl'>
|
||||
<FormattedMessage id='alert.unexpected.message' defaultMessage='Something went wrong.' />
|
||||
</h1>
|
||||
<p className='text-lg text-gray-700 dark:text-gray-600'>
|
||||
<FormattedMessage
|
||||
id='alert.unexpected.body'
|
||||
defaultMessage="We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out)."
|
||||
values={{
|
||||
clearCookies: (
|
||||
<a href='/' onClick={clearCookies} className='text-primary-600 hover:underline dark:text-accent-blue'>
|
||||
<FormattedMessage
|
||||
id='alert.unexpected.clear_cookies'
|
||||
defaultMessage='clear cookies and browser data'
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
return {
|
||||
siteTitle: state.instance.title,
|
||||
logo,
|
||||
links,
|
||||
};
|
||||
}
|
||||
<Text theme='muted'>
|
||||
<Text weight='medium' tag='span' theme='muted'>{sourceCode.displayName}:</Text>
|
||||
|
||||
export default connect(mapStateToProps)(ErrorBoundary);
|
||||
{' '}{sourceCode.version}
|
||||
</Text>
|
||||
|
||||
<div className='mt-10'>
|
||||
<a href='/' className='text-base font-medium text-primary-600 hover:underline dark:text-accent-blue'>
|
||||
<FormattedMessage id='alert.unexpected.return_home' defaultMessage='Return Home' />
|
||||
{' '}
|
||||
<span className='inline-block rtl:rotate-180' aria-hidden='true'>→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isProduction && (
|
||||
<div className='mx-auto max-w-lg space-y-4 py-16'>
|
||||
{errorText && (
|
||||
<textarea
|
||||
ref={textarea}
|
||||
className='block h-48 w-full rounded-md border-gray-300 bg-gray-100 p-4 font-mono text-gray-900 shadow-sm focus:border-primary-500 focus:ring-2 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 sm:text-sm'
|
||||
value={errorText}
|
||||
onClick={handleCopy}
|
||||
dir='ltr'
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
|
||||
{browser && (
|
||||
<Stack>
|
||||
<Text weight='semibold'><FormattedMessage id='alert.unexpected.browser' defaultMessage='Browser' /></Text>
|
||||
<Text theme='muted'>{browser.getBrowserName()} {browser.getBrowserVersion()}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className='mx-auto w-full max-w-7xl shrink-0 px-4 sm:px-6 lg:px-8'>
|
||||
<HStack justifyContent='center' space={4} element='nav'>
|
||||
{links.get('status') && (
|
||||
<>
|
||||
<a href={links.get('status')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||
<FormattedMessage id='alert.unexpected.links.status' defaultMessage='Status' />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
||||
{links.get('help') && (
|
||||
<>
|
||||
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
||||
<a href={links.get('help')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||
<FormattedMessage id='alert.unexpected.links.help' defaultMessage='Help Center' />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
||||
{links.get('support') && (
|
||||
<>
|
||||
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
||||
<a href={links.get('support')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||
<FormattedMessage id='alert.unexpected.links.support' defaultMessage='Support' />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<ErrorBoundary fallback={fallback} onError={handleError}>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default SiteErrorBoundary;
|
||||
|
|
|
@ -6,6 +6,7 @@ import { CompatRouter } from 'react-router-dom-v5-compat';
|
|||
import { ScrollContext } from 'react-router-scroll-4';
|
||||
|
||||
import * as BuildConfig from 'soapbox/build-config';
|
||||
import SiteErrorBoundary from 'soapbox/components/error-boundary';
|
||||
import LoadingScreen from 'soapbox/components/loading-screen';
|
||||
import {
|
||||
ModalContainer,
|
||||
|
@ -18,8 +19,6 @@ import {
|
|||
} from 'soapbox/hooks';
|
||||
import { useCachedLocationHandler } from 'soapbox/utils/redirect';
|
||||
|
||||
import ErrorBoundary from '../components/error-boundary';
|
||||
|
||||
const GdprBanner = React.lazy(() => import('soapbox/components/gdpr-banner'));
|
||||
const EmbeddedStatus = React.lazy(() => import('soapbox/features/embedded-status'));
|
||||
const UI = React.lazy(() => import('soapbox/features/ui'));
|
||||
|
@ -42,7 +41,7 @@ const SoapboxMount = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<SiteErrorBoundary>
|
||||
<BrowserRouter basename={BuildConfig.FE_SUBDIRECTORY}>
|
||||
<CompatRouter>
|
||||
<ScrollContext shouldUpdateScroll={shouldUpdateScroll}>
|
||||
|
@ -90,7 +89,7 @@ const SoapboxMount = () => {
|
|||
</ScrollContext>
|
||||
</CompatRouter>
|
||||
</BrowserRouter>
|
||||
</ErrorBoundary>
|
||||
</SiteErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -7186,6 +7186,13 @@ react-error-boundary@^3.1.0, react-error-boundary@^3.1.4:
|
|||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
|
||||
react-error-boundary@^4.0.11:
|
||||
version "4.0.11"
|
||||
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.11.tgz#36bf44de7746714725a814630282fee83a7c9a1c"
|
||||
integrity sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
|
||||
react-event-listener@^0.6.0:
|
||||
version "0.6.6"
|
||||
resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.6.6.tgz#758f7b991cad9086dd39fd29fad72127e1d8962a"
|
||||
|
|
Loading…
Reference in a new issue