Repository: facebook/react
Pull request: Errors thrown by Store can be dismissed in boundary

Split off from #21516

Sometimes errors thrown by the store are recoverable. This PR updates the error boundary so that you can dismiss the error (preferably after reporting it).

![Error thrown by Store and dismissed in error boundary](https://user-images.githubusercontent.com/29597/118669099-77b83a00-b7c3-11eb-803b-8b0d7eb06d1d.gif)


Changed files:
filename: packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js
patch: @@ -21,13 +21,15 @@ type Props = {|
 
 type State = {|
   callStack: string | null,
+  canDismiss: boolean,
   componentStack: string | null,
   errorMessage: string | null,
   hasError: boolean,
 |};
 
 const InitialState: State = {
   callStack: null,
+  canDismiss: false,
   componentStack: null,
   errorMessage: null,
   hasError: false,
@@ -77,13 +79,20 @@ export default class ErrorBoundary extends Component<Props, State> {
 
   render() {
     const {children} = this.props;
-    const {callStack, componentStack, errorMessage, hasError} = this.state;
+    const {
+      callStack,
+      canDismiss,
+      componentStack,
+      errorMessage,
+      hasError,
+    } = this.state;
 
     if (hasError) {
       return (
         <ErrorView
           callStack={callStack}
           componentStack={componentStack}
+          dismissError={canDismiss ? this._dismissError : null}
           errorMessage={errorMessage}>
           <Suspense fallback={<SearchingGitHubIssues />}>
             <SuspendingErrorView
@@ -99,9 +108,16 @@ export default class ErrorBoundary extends Component<Props, State> {
     return children;
   }
 
+  _dismissError = () => {
+    this.setState(InitialState);
+  };
+
   _onStoreError = (error: Error) => {
     if (!this.state.hasError) {
-      this.setState(ErrorBoundary.getDerivedStateFromError(error));
+      this.setState({
+        ...ErrorBoundary.getDerivedStateFromError(error),
+        canDismiss: true,
+      });
     }
   };
 }

filename: packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorView.js
patch: @@ -8,27 +8,39 @@
  */
 
 import * as React from 'react';
+import Button from '../Button';
+import ButtonIcon from '../ButtonIcon';
 import styles from './shared.css';
 
 type Props = {|
   callStack: string | null,
   children: React$Node,
   componentStack: string | null,
+  dismissError: Function | null,
   errorMessage: string | null,
 |};
 
 export default function ErrorView({
   callStack,
   children,
   componentStack,
+  dismissError = null,
   errorMessage,
 }: Props) {
   return (
     <div className={styles.ErrorBoundary}>
       {children}
       <div className={styles.ErrorInfo}>
-        <div className={styles.Header}>
-          Uncaught Error: {errorMessage || ''}
+        <div className={styles.HeaderRow}>
+          <div className={styles.Header}>
+            Uncaught Error: {errorMessage || ''}
+          </div>
+          {dismissError !== null && (
+            <Button className={styles.CloseButton} onClick={dismissError}>
+              Dismiss
+              <ButtonIcon className={styles.CloseButtonIcon} type="close" />
+            </Button>
+          )}
         </div>
         {!!callStack && (
           <div className={styles.Stack}>

filename: packages/react-devtools-shared/src/devtools/views/ErrorBoundary/shared.css
patch: @@ -37,12 +37,22 @@
   overflow: auto;
 }
 
-.Header {
+.HeaderRow {
+  display: flex;
+  flex-direction: row;
   font-size: var(--font-size-sans-large);
   font-weight: bold;
   color: var(--color-error-text);
 }
 
+.Header {
+  flex: 1 1 auto;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  min-width: 0;
+}
+
 .Stack {
   margin-top: 0.5rem;
   white-space: pre-wrap;
@@ -75,9 +85,21 @@
 .ReproSteps {
   margin-left: 0.25rem;
   color: var(--color-console-warning-text);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  min-width: 0;
 }
 
 .UpdateExistingIssuePrompt {
   margin-right: 0.25rem;
   color: var(--color-console-warning-text);
+}
+
+.CloseButton {
+  font-weight: bold;
+}
+
+.CloseButtonIcon {
+  margin-left: 0.25rem;
 }
\ No newline at end of file