@samitouri / QOS-React-2 / commits / 5f3b376c56

Show different error boundary UI for timeouts than normal errors (#22483)

Brian Vaughn committed Oct 1, 2021 at 15:03 UTC 5f3b376c56c6c523e7018f5128a768cbd67a8a6f
7 files changed +146 -31
packages/react-devtools-shared/src/TimeoutError.js new
+21
@@ -0,0 +1,21 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export default class TimeoutError extends Error {
11 + constructor(message: string) {
12 + super(message);
13 +
14 + // Maintains proper stack trace for where our error was thrown (only available on V8)
15 + if (Error.captureStackTrace) {
16 + Error.captureStackTrace(this, TimeoutError);
17 + }
18 +
19 + this.name = 'TimeoutError';
20 + }
21 +}
packages/react-devtools-shared/src/backendAPI.js
+4 -3
@@ -10,6 +10,7 @@
10 import {hydrate, fillInPath} from 'react-devtools-shared/src/hydration';
11 import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
12 import Store from 'react-devtools-shared/src/devtools/store';
13 +import TimeoutError from 'react-devtools-shared/src/TimeoutError';
14
15 import type {
16 InspectedElement as InspectedElementBackend,
@@ -102,6 +103,7 @@ export function inspectElement({
103 requestID,
104 'inspectedElement',
105 bridge,
106 + `Timed out while inspecting element ${id}.`,
107 );
108
109 bridge.send('inspectElement', {
@@ -144,6 +146,7 @@ function getPromiseForRequestID<T>(
146 requestID: number,
147 eventType: $Keys<BackendEvents>,
148 bridge: FrontendBridge,
149 + timeoutMessage: string,
150 ): Promise<T> {
151 return new Promise((resolve, reject) => {
152 const cleanup = () => {
@@ -161,9 +164,7 @@ function getPromiseForRequestID<T>(
164
165 const onTimeout = () => {
166 cleanup();
164 - reject(
165 - new Error(`Timed out waiting for event '${eventType}' from bridge`),
166 - );
167 + reject(new TimeoutError(timeoutMessage));
168 };
169
170 bridge.addListener(eventType, onInspectedElement);
packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js
+38 -17
@@ -13,6 +13,8 @@ import Store from 'react-devtools-shared/src/devtools/store';
13 import ErrorView from './ErrorView';
14 import SearchingGitHubIssues from './SearchingGitHubIssues';
15 import SuspendingErrorView from './SuspendingErrorView';
16 +import TimeoutView from './TimeoutView';
17 +import TimeoutError from 'react-devtools-shared/src/TimeoutError';
18
19 type Props = {|
20 children: React$Node,
@@ -27,6 +29,7 @@ type State = {|
29 componentStack: string | null,
30 errorMessage: string | null,
31 hasError: boolean,
32 + isTimeout: boolean,
33 |};
34
35 const InitialState: State = {
@@ -35,6 +38,7 @@ const InitialState: State = {
38 componentStack: null,
39 errorMessage: null,
40 hasError: false,
41 + isTimeout: false,
42 };
43
44 export default class ErrorBoundary extends Component<Props, State> {
@@ -48,6 +52,8 @@ export default class ErrorBoundary extends Component<Props, State> {
52 ? error.message
53 : String(error);
54
55 + const isTimeout = error instanceof TimeoutError;
56 +
57 const callStack =
58 typeof error === 'object' &&
59 error !== null &&
@@ -62,6 +68,7 @@ export default class ErrorBoundary extends Component<Props, State> {
68 callStack,
69 errorMessage,
70 hasError: true,
71 + isTimeout,
72 };
73 }
74
@@ -93,26 +100,40 @@ export default class ErrorBoundary extends Component<Props, State> {
100 componentStack,
101 errorMessage,
102 hasError,
103 + isTimeout,
104 } = this.state;
105
106 if (hasError) {
99 - return (
100 - <ErrorView
101 - callStack={callStack}
102 - componentStack={componentStack}
103 - dismissError={
104 - canDismissProp || canDismissState ? this._dismissError : null
105 - }
106 - errorMessage={errorMessage}>
107 - <Suspense fallback={<SearchingGitHubIssues />}>
108 - <SuspendingErrorView
109 - callStack={callStack}
110 - componentStack={componentStack}
111 - errorMessage={errorMessage}
112 - />
113 - </Suspense>
114 - </ErrorView>
115 - );
107 + if (isTimeout) {
108 + return (
109 + <TimeoutView
110 + callStack={callStack}
111 + componentStack={componentStack}
112 + dismissError={
113 + canDismissProp || canDismissState ? this._dismissError : null
114 + }
115 + errorMessage={errorMessage}
116 + />
117 + );
118 + } else {
119 + return (
120 + <ErrorView
121 + callStack={callStack}
122 + componentStack={componentStack}
123 + dismissError={
124 + canDismissProp || canDismissState ? this._dismissError : null
125 + }
126 + errorMessage={errorMessage}>
127 + <Suspense fallback={<SearchingGitHubIssues />}>
128 + <SuspendingErrorView
129 + callStack={callStack}
130 + componentStack={componentStack}
131 + errorMessage={errorMessage}
132 + />
133 + </Suspense>
134 + </ErrorView>
135 + );
136 + }
137 }
138
139 return children;
packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorView.js
+3 -3
@@ -32,7 +32,7 @@ export default function ErrorView({
32 {children}
33 <div className={styles.ErrorInfo}>
34 <div className={styles.HeaderRow}>
35 - <div className={styles.Header}>
35 + <div className={styles.ErrorHeader}>
36 Uncaught Error: {errorMessage || ''}
37 </div>
38 {dismissError !== null && (
@@ -43,12 +43,12 @@ export default function ErrorView({
43 )}
44 </div>
45 {!!callStack && (
46 - <div className={styles.Stack}>
46 + <div className={styles.ErrorStack}>
47 The error was thrown {callStack.trim()}
48 </div>
49 )}
50 {!!componentStack && (
51 - <div className={styles.Stack}>
51 + <div className={styles.ErrorStack}>
52 The error occurred {componentStack.trim()}
53 </div>
54 )}
packages/react-devtools-shared/src/devtools/views/ErrorBoundary/TimeoutView.js new
+51
@@ -0,0 +1,51 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import * as React from 'react';
11 +import Button from '../Button';
12 +import ButtonIcon from '../ButtonIcon';
13 +import styles from './shared.css';
14 +
15 +type Props = {|
16 + callStack: string | null,
17 + children: React$Node,
18 + componentStack: string | null,
19 + dismissError: Function,
20 + errorMessage: string | null,
21 +|};
22 +
23 +export default function TimeoutView({
24 + callStack,
25 + children,
26 + componentStack,
27 + dismissError = null,
28 + errorMessage,
29 +}: Props) {
30 + return (
31 + <div className={styles.ErrorBoundary}>
32 + {children}
33 + <div className={styles.ErrorInfo}>
34 + <div className={styles.HeaderRow}>
35 + <div className={styles.TimeoutHeader}>
36 + {errorMessage || 'Timed out waiting'}
37 + </div>
38 + <Button className={styles.CloseButton} onClick={dismissError}>
39 + Retry
40 + <ButtonIcon className={styles.CloseButtonIcon} type="close" />
41 + </Button>
42 + </div>
43 + {!!componentStack && (
44 + <div className={styles.TimeoutStack}>
45 + The timeout occurred {componentStack.trim()}
46 + </div>
47 + )}
48 + </div>
49 + </div>
50 + );
51 +}
packages/react-devtools-shared/src/devtools/views/ErrorBoundary/shared.css
+24 -5
@@ -30,6 +30,7 @@
30 background-color: var(--color-background);
31 display: flex;
32 flex-direction: column;
33 + border: 1px solid var(--color-border);
34 }
35
36 .ErrorInfo {
@@ -42,10 +43,10 @@
43 flex-direction: row;
44 font-size: var(--font-size-sans-large);
45 font-weight: bold;
45 - color: var(--color-error-text);
46 }
47
48 -.Header {
48 +.ErrorHeader,
49 +.TimeoutHeader {
50 flex: 1 1 auto;
51 overflow: hidden;
52 text-overflow: ellipsis;
@@ -53,17 +54,35 @@
54 min-width: 0;
55 }
56
56 -.Stack {
57 +.ErrorHeader {
58 + color: var(--color-error-text);
59 +}
60 +.TimeoutHeader {
61 + color: var(--color-text);
62 +}
63 +
64 +.ErrorStack,
65 +.TimeoutStack {
66 margin-top: 0.5rem;
67 white-space: pre-wrap;
68 font-family: var(--font-family-monospace);
69 font-size: var(--font-size-monospace-normal);
70 -webkit-font-smoothing: initial;
71 + border-radius: 0.25rem;
72 + padding: 0.5rem;
73 + overflow: auto;
74 +}
75 +
76 +.ErrorStack {
77 background-color: var(--color-error-background);
78 border: 1px solid var(--color-error-border);
79 color: var(--color-error-text);
65 - border-radius: 0.25rem;
66 - padding: 0.5rem;
80 +}
81 +
82 +.TimeoutStack {
83 + background-color: var(--color-console-warning-background);
84 + color: var(--color-console-warning-text);
85 + border: var(--color-console-warning-border)
86 }
87
88 .LoadingIcon {
packages/react-devtools-shared/src/inspectedElementCache.js
+5 -3
@@ -38,7 +38,7 @@ type ResolvedRecord<T> = {|
38
39 type RejectedRecord = {|
40 status: 2,
41 - value: string,
41 + value: Error | string,
42 |};
43
44 type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
@@ -113,7 +113,9 @@ export function inspectElement(
113 if (rendererID == null) {
114 const rejectedRecord = ((newRecord: any): RejectedRecord);
115 rejectedRecord.status = Rejected;
116 - rejectedRecord.value = `Could not inspect element with id "${element.id}". No renderer found.`;
116 + rejectedRecord.value = new Error(
117 + `Could not inspect element with id "${element.id}". No renderer found.`,
118 + );
119
120 map.set(element, record);
121
@@ -139,7 +141,7 @@ export function inspectElement(
141
142 const rejectedRecord = ((newRecord: any): RejectedRecord);
143 rejectedRecord.status = Rejected;
142 - rejectedRecord.value = `Could not inspect element with id "${element.id}". Error thrown:\n${error.message}`;
144 + rejectedRecord.value = error;
145
146 wake();
147 },