Rewrite ErrorBoundary as a functional component using react-error-boundary

This commit is contained in:
Alex Gleason 2023-10-21 15:32:37 -05:00
parent c5d527a667
commit d7ea38cf22
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
4 changed files with 154 additions and 189 deletions

View file

@ -134,6 +134,7 @@
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-datepicker": "^4.8.0", "react-datepicker": "^4.8.0",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
"react-error-boundary": "^4.0.11",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-hot-toast": "^2.4.0", "react-hot-toast": "^2.4.0",
"react-hotkeys": "^1.1.4", "react-hotkeys": "^1.1.4",

View file

@ -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 { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { getSoapboxConfig } from 'soapbox/actions/soapbox'; import { NODE_ENV } from 'soapbox/build-config';
import * as BuildConfig from 'soapbox/build-config';
import { HStack, Text, Stack } from 'soapbox/components/ui'; import { HStack, Text, Stack } from 'soapbox/components/ui';
import { useSoapboxConfig } from 'soapbox/hooks';
import { captureSentryException } from 'soapbox/sentry'; import { captureSentryException } from 'soapbox/sentry';
import KVStore from 'soapbox/storage/kv-store'; import KVStore from 'soapbox/storage/kv-store';
import sourceCode from 'soapbox/utils/code'; import sourceCode from 'soapbox/utils/code';
@ -12,72 +12,23 @@ import { unregisterSW } from 'soapbox/utils/sw';
import SiteLogo from './site-logo'; import SiteLogo from './site-logo';
import type { RootState } from 'soapbox/store'; interface ISiteErrorBoundary {
interface Props extends ReturnType<typeof mapStateToProps> {
children: React.ReactNode; children: React.ReactNode;
} }
type State = { /** Application-level error boundary. Fills the whole screen. */
hasError: boolean; const SiteErrorBoundary: React.FC<ISiteErrorBoundary> = ({ children }) => {
error: any; const { links } = useSoapboxConfig();
componentStack: any; const textarea = useRef<HTMLTextAreaElement>(null);
browser?: Bowser.Parser.Parser;
}
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 = { const isProduction = NODE_ENV === 'production';
hasError: false, const errorText = error + componentStack;
error: undefined,
componentStack: undefined,
browser: undefined,
};
textarea: HTMLTextAreaElement | null = null; const clearCookies: React.MouseEventHandler = (e) => {
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) => {
localStorage.clear(); localStorage.clear();
sessionStorage.clear(); sessionStorage.clear();
KVStore.clear(); KVStore.clear();
@ -88,19 +39,36 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
} }
}; };
render() { const handleCopy: React.MouseEventHandler = () => {
const { browser, hasError } = this.state; if (!textarea.current) return;
const { children, links } = this.props;
if (!hasError) { textarea.current.select();
return children; textarea.current.setSelectionRange(0, 99999);
document.execCommand('copy');
};
function handleError(error: Error, info: ErrorInfo) {
setError(error);
setComponentStack(info.componentStack);
captureSentryException(error, {
tags: {
// Allow page crashes to be easily searched in Sentry.
ErrorBoundary: 'yes',
},
});
import('bowser')
.then(({ default: Bowser }) => setBrowser(Bowser.getParser(window.navigator.userAgent)))
.catch(() => {});
} }
const isProduction = BuildConfig.NODE_ENV === 'production'; function goHome() {
location.href = '/';
}
const errorText = this.getErrorText(); const fallback = (
return (
<div className='flex h-screen flex-col bg-white pb-12 pt-16 dark:bg-primary-900'> <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'> <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'> <div className='flex shrink-0 justify-center'>
@ -120,7 +88,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
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)." 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={{ values={{
clearCookies: ( clearCookies: (
<a href='/' onClick={this.clearCookies} className='text-primary-600 hover:underline dark:text-accent-blue'> <a href='/' onClick={clearCookies} className='text-primary-600 hover:underline dark:text-accent-blue'>
<FormattedMessage <FormattedMessage
id='alert.unexpected.clear_cookies' id='alert.unexpected.clear_cookies'
defaultMessage='clear cookies and browser data' defaultMessage='clear cookies and browser data'
@ -150,10 +118,10 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
<div className='mx-auto max-w-lg space-y-4 py-16'> <div className='mx-auto max-w-lg space-y-4 py-16'>
{errorText && ( {errorText && (
<textarea <textarea
ref={this.setTextareaRef} 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' 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} value={errorText}
onClick={this.handleCopy} onClick={handleCopy}
dir='ltr' dir='ltr'
readOnly readOnly
/> />
@ -201,22 +169,12 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
</footer> </footer>
</div> </div>
); );
}
} return (
<ErrorBoundary fallback={fallback} onError={handleError}>
function goHome() { {children}
location.href = '/'; </ErrorBoundary>
} );
function mapStateToProps(state: RootState) {
const { links, logo } = getSoapboxConfig(state);
return {
siteTitle: state.instance.title,
logo,
links,
}; };
}
export default connect(mapStateToProps)(ErrorBoundary); export default SiteErrorBoundary;

View file

@ -6,6 +6,7 @@ import { CompatRouter } from 'react-router-dom-v5-compat';
import { ScrollContext } from 'react-router-scroll-4'; import { ScrollContext } from 'react-router-scroll-4';
import * as BuildConfig from 'soapbox/build-config'; import * as BuildConfig from 'soapbox/build-config';
import SiteErrorBoundary from 'soapbox/components/error-boundary';
import LoadingScreen from 'soapbox/components/loading-screen'; import LoadingScreen from 'soapbox/components/loading-screen';
import { import {
ModalContainer, ModalContainer,
@ -18,8 +19,6 @@ import {
} from 'soapbox/hooks'; } from 'soapbox/hooks';
import { useCachedLocationHandler } from 'soapbox/utils/redirect'; import { useCachedLocationHandler } from 'soapbox/utils/redirect';
import ErrorBoundary from '../components/error-boundary';
const GdprBanner = React.lazy(() => import('soapbox/components/gdpr-banner')); const GdprBanner = React.lazy(() => import('soapbox/components/gdpr-banner'));
const EmbeddedStatus = React.lazy(() => import('soapbox/features/embedded-status')); const EmbeddedStatus = React.lazy(() => import('soapbox/features/embedded-status'));
const UI = React.lazy(() => import('soapbox/features/ui')); const UI = React.lazy(() => import('soapbox/features/ui'));
@ -42,7 +41,7 @@ const SoapboxMount = () => {
}; };
return ( return (
<ErrorBoundary> <SiteErrorBoundary>
<BrowserRouter basename={BuildConfig.FE_SUBDIRECTORY}> <BrowserRouter basename={BuildConfig.FE_SUBDIRECTORY}>
<CompatRouter> <CompatRouter>
<ScrollContext shouldUpdateScroll={shouldUpdateScroll}> <ScrollContext shouldUpdateScroll={shouldUpdateScroll}>
@ -90,7 +89,7 @@ const SoapboxMount = () => {
</ScrollContext> </ScrollContext>
</CompatRouter> </CompatRouter>
</BrowserRouter> </BrowserRouter>
</ErrorBoundary> </SiteErrorBoundary>
); );
}; };

View file

@ -7186,6 +7186,13 @@ react-error-boundary@^3.1.0, react-error-boundary@^3.1.4:
dependencies: dependencies:
"@babel/runtime" "^7.12.5" "@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: react-event-listener@^0.6.0:
version "0.6.6" version "0.6.6"
resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.6.6.tgz#758f7b991cad9086dd39fd29fad72127e1d8962a" resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.6.6.tgz#758f7b991cad9086dd39fd29fad72127e1d8962a"