From c6146f63ac1553d2865ab919e1f38e90661ba084 Mon Sep 17 00:00:00 2001 From: jero Date: Tue, 3 Sep 2019 17:45:33 -0500 Subject: [PATCH] feat(core): add ErrorBoundary component catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed improves #33 --- .../core/ErrorBoundary/ErrorBoundary.js | 87 +++++++++++++++++++ .../ErrorBoundaryFallbackComponent.js | 38 ++++++++ .../components/core/ErrorBoundary/index.js | 7 ++ 3 files changed, 132 insertions(+) create mode 100644 src/app/components/core/ErrorBoundary/ErrorBoundary.js create mode 100644 src/app/components/core/ErrorBoundary/ErrorBoundaryFallbackComponent.js create mode 100644 src/app/components/core/ErrorBoundary/index.js diff --git a/src/app/components/core/ErrorBoundary/ErrorBoundary.js b/src/app/components/core/ErrorBoundary/ErrorBoundary.js new file mode 100644 index 00000000..0087e5c3 --- /dev/null +++ b/src/app/components/core/ErrorBoundary/ErrorBoundary.js @@ -0,0 +1,87 @@ +// @flow strict +import React, { Component } from 'react'; +import type { ComponentType } from 'react'; + +import ErrorBoundaryFallbackComponent from './ErrorBoundaryFallbackComponent'; + +type Props = { + children?: *, + FallbackComponent: ComponentType<*>, + onError?: (error: Error, componentStack: string) => void, +}; + +type ErrorInfo = { + componentStack: string, +}; + +type State = { + error: ?Error, + info: ?ErrorInfo, +}; + +class ErrorBoundary extends Component { + static defaultProps = { + FallbackComponent: ErrorBoundaryFallbackComponent, + }; + + state = { + error: null, + info: null, + }; + + componentDidCatch(error: Error, info: ErrorInfo): void { + const { onError } = this.props; + + if (typeof onError === 'function') { + try { + onError.call(this, error, info ? info.componentStack : ''); + // eslint-disable-next-line no-empty + } catch (ignoredError) {} + } + + this.setState({ error, info }); + } + + render() { + const { children, FallbackComponent } = this.props; + const { error, info } = this.state; + + if (error != null) { + return ( + + ); + } + + return children || null; + } +} + +export const withErrorBoundary = ( + SomeComponent: ComponentType<*>, + FallbackComponent: ComponentType<*>, + onError: (Error) => void, +): ComponentType<{}> => { + const Wrapped = (props) => ( + + + + ); + + // Format for display in DevTools + const name = SomeComponent.displayName || SomeComponent.name; + + Wrapped.displayName = name + ? `WithErrorBoundary(${name})` + : 'WithErrorBoundary'; + + return Wrapped; +}; + +export default ErrorBoundary; diff --git a/src/app/components/core/ErrorBoundary/ErrorBoundaryFallbackComponent.js b/src/app/components/core/ErrorBoundary/ErrorBoundaryFallbackComponent.js new file mode 100644 index 00000000..a6644ed4 --- /dev/null +++ b/src/app/components/core/ErrorBoundary/ErrorBoundaryFallbackComponent.js @@ -0,0 +1,38 @@ +// @flow strict + +import React from 'react'; + +type Props = { + componentStack: string, + error: Error, +}; + +const toTitle = ( + error: Error, + componentStack: string, +): string => (`${error.toString()}\n\nThis is located at:${componentStack}`); + +const style = { + alignItems: 'center', + boxSizing: 'border-box', + color: '#FFF', + cursor: 'help', + display: 'flex', + flexDirection: 'column', + height: '100%', + maxHeight: '100vh', + maxWidth: '100vw', + textAlign: 'center', + width: '100%', +}; + +const ErrorBoundaryFallbackComponent = ({ componentStack, error }: Props) => ( +
+
{componentStack}
+
+); + +export default ErrorBoundaryFallbackComponent; diff --git a/src/app/components/core/ErrorBoundary/index.js b/src/app/components/core/ErrorBoundary/index.js new file mode 100644 index 00000000..ed581689 --- /dev/null +++ b/src/app/components/core/ErrorBoundary/index.js @@ -0,0 +1,7 @@ +// @flow strict +import ErrorBoundaryFallbackComponent from './ErrorBoundaryFallbackComponent'; +import ErrorBoundary, { withErrorBoundary } from './ErrorBoundary'; + +export default ErrorBoundary; + +export { ErrorBoundary, withErrorBoundary, ErrorBoundaryFallbackComponent };