mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
Show different error boundary UI for timeouts than normal errors (#22483)
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export default class TimeoutError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
|
||||
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, TimeoutError);
|
||||
}
|
||||
|
||||
this.name = 'TimeoutError';
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -10,6 +10,7 @@
|
||||
import {hydrate, fillInPath} from 'react-devtools-shared/src/hydration';
|
||||
import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
|
||||
import Store from 'react-devtools-shared/src/devtools/store';
|
||||
import TimeoutError from 'react-devtools-shared/src/TimeoutError';
|
||||
|
||||
import type {
|
||||
InspectedElement as InspectedElementBackend,
|
||||
@@ -102,6 +103,7 @@ export function inspectElement({
|
||||
requestID,
|
||||
'inspectedElement',
|
||||
bridge,
|
||||
`Timed out while inspecting element ${id}.`,
|
||||
);
|
||||
|
||||
bridge.send('inspectElement', {
|
||||
@@ -144,6 +146,7 @@ function getPromiseForRequestID<T>(
|
||||
requestID: number,
|
||||
eventType: $Keys<BackendEvents>,
|
||||
bridge: FrontendBridge,
|
||||
timeoutMessage: string,
|
||||
): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
@@ -161,9 +164,7 @@ function getPromiseForRequestID<T>(
|
||||
|
||||
const onTimeout = () => {
|
||||
cleanup();
|
||||
reject(
|
||||
new Error(`Timed out waiting for event '${eventType}' from bridge`),
|
||||
);
|
||||
reject(new TimeoutError(timeoutMessage));
|
||||
};
|
||||
|
||||
bridge.addListener(eventType, onInspectedElement);
|
||||
|
||||
+38
-17
@@ -13,6 +13,8 @@ import Store from 'react-devtools-shared/src/devtools/store';
|
||||
import ErrorView from './ErrorView';
|
||||
import SearchingGitHubIssues from './SearchingGitHubIssues';
|
||||
import SuspendingErrorView from './SuspendingErrorView';
|
||||
import TimeoutView from './TimeoutView';
|
||||
import TimeoutError from 'react-devtools-shared/src/TimeoutError';
|
||||
|
||||
type Props = {|
|
||||
children: React$Node,
|
||||
@@ -27,6 +29,7 @@ type State = {|
|
||||
componentStack: string | null,
|
||||
errorMessage: string | null,
|
||||
hasError: boolean,
|
||||
isTimeout: boolean,
|
||||
|};
|
||||
|
||||
const InitialState: State = {
|
||||
@@ -35,6 +38,7 @@ const InitialState: State = {
|
||||
componentStack: null,
|
||||
errorMessage: null,
|
||||
hasError: false,
|
||||
isTimeout: false,
|
||||
};
|
||||
|
||||
export default class ErrorBoundary extends Component<Props, State> {
|
||||
@@ -48,6 +52,8 @@ export default class ErrorBoundary extends Component<Props, State> {
|
||||
? error.message
|
||||
: String(error);
|
||||
|
||||
const isTimeout = error instanceof TimeoutError;
|
||||
|
||||
const callStack =
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
@@ -62,6 +68,7 @@ export default class ErrorBoundary extends Component<Props, State> {
|
||||
callStack,
|
||||
errorMessage,
|
||||
hasError: true,
|
||||
isTimeout,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -93,26 +100,40 @@ export default class ErrorBoundary extends Component<Props, State> {
|
||||
componentStack,
|
||||
errorMessage,
|
||||
hasError,
|
||||
isTimeout,
|
||||
} = this.state;
|
||||
|
||||
if (hasError) {
|
||||
return (
|
||||
<ErrorView
|
||||
callStack={callStack}
|
||||
componentStack={componentStack}
|
||||
dismissError={
|
||||
canDismissProp || canDismissState ? this._dismissError : null
|
||||
}
|
||||
errorMessage={errorMessage}>
|
||||
<Suspense fallback={<SearchingGitHubIssues />}>
|
||||
<SuspendingErrorView
|
||||
callStack={callStack}
|
||||
componentStack={componentStack}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
</Suspense>
|
||||
</ErrorView>
|
||||
);
|
||||
if (isTimeout) {
|
||||
return (
|
||||
<TimeoutView
|
||||
callStack={callStack}
|
||||
componentStack={componentStack}
|
||||
dismissError={
|
||||
canDismissProp || canDismissState ? this._dismissError : null
|
||||
}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ErrorView
|
||||
callStack={callStack}
|
||||
componentStack={componentStack}
|
||||
dismissError={
|
||||
canDismissProp || canDismissState ? this._dismissError : null
|
||||
}
|
||||
errorMessage={errorMessage}>
|
||||
<Suspense fallback={<SearchingGitHubIssues />}>
|
||||
<SuspendingErrorView
|
||||
callStack={callStack}
|
||||
componentStack={componentStack}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
</Suspense>
|
||||
</ErrorView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return children;
|
||||
|
||||
+3
-3
@@ -32,7 +32,7 @@ export default function ErrorView({
|
||||
{children}
|
||||
<div className={styles.ErrorInfo}>
|
||||
<div className={styles.HeaderRow}>
|
||||
<div className={styles.Header}>
|
||||
<div className={styles.ErrorHeader}>
|
||||
Uncaught Error: {errorMessage || ''}
|
||||
</div>
|
||||
{dismissError !== null && (
|
||||
@@ -43,12 +43,12 @@ export default function ErrorView({
|
||||
)}
|
||||
</div>
|
||||
{!!callStack && (
|
||||
<div className={styles.Stack}>
|
||||
<div className={styles.ErrorStack}>
|
||||
The error was thrown {callStack.trim()}
|
||||
</div>
|
||||
)}
|
||||
{!!componentStack && (
|
||||
<div className={styles.Stack}>
|
||||
<div className={styles.ErrorStack}>
|
||||
The error occurred {componentStack.trim()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
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,
|
||||
errorMessage: string | null,
|
||||
|};
|
||||
|
||||
export default function TimeoutView({
|
||||
callStack,
|
||||
children,
|
||||
componentStack,
|
||||
dismissError = null,
|
||||
errorMessage,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className={styles.ErrorBoundary}>
|
||||
{children}
|
||||
<div className={styles.ErrorInfo}>
|
||||
<div className={styles.HeaderRow}>
|
||||
<div className={styles.TimeoutHeader}>
|
||||
{errorMessage || 'Timed out waiting'}
|
||||
</div>
|
||||
<Button className={styles.CloseButton} onClick={dismissError}>
|
||||
Retry
|
||||
<ButtonIcon className={styles.CloseButtonIcon} type="close" />
|
||||
</Button>
|
||||
</div>
|
||||
{!!componentStack && (
|
||||
<div className={styles.TimeoutStack}>
|
||||
The timeout occurred {componentStack.trim()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -30,6 +30,7 @@
|
||||
background-color: var(--color-background);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.ErrorInfo {
|
||||
@@ -42,10 +43,10 @@
|
||||
flex-direction: row;
|
||||
font-size: var(--font-size-sans-large);
|
||||
font-weight: bold;
|
||||
color: var(--color-error-text);
|
||||
}
|
||||
|
||||
.Header {
|
||||
.ErrorHeader,
|
||||
.TimeoutHeader {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -53,17 +54,35 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.Stack {
|
||||
.ErrorHeader {
|
||||
color: var(--color-error-text);
|
||||
}
|
||||
.TimeoutHeader {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.ErrorStack,
|
||||
.TimeoutStack {
|
||||
margin-top: 0.5rem;
|
||||
white-space: pre-wrap;
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-normal);
|
||||
-webkit-font-smoothing: initial;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ErrorStack {
|
||||
background-color: var(--color-error-background);
|
||||
border: 1px solid var(--color-error-border);
|
||||
color: var(--color-error-text);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.TimeoutStack {
|
||||
background-color: var(--color-console-warning-background);
|
||||
color: var(--color-console-warning-text);
|
||||
border: var(--color-console-warning-border)
|
||||
}
|
||||
|
||||
.LoadingIcon {
|
||||
|
||||
@@ -38,7 +38,7 @@ type ResolvedRecord<T> = {|
|
||||
|
||||
type RejectedRecord = {|
|
||||
status: 2,
|
||||
value: string,
|
||||
value: Error | string,
|
||||
|};
|
||||
|
||||
type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
|
||||
@@ -113,7 +113,9 @@ export function inspectElement(
|
||||
if (rendererID == null) {
|
||||
const rejectedRecord = ((newRecord: any): RejectedRecord);
|
||||
rejectedRecord.status = Rejected;
|
||||
rejectedRecord.value = `Could not inspect element with id "${element.id}". No renderer found.`;
|
||||
rejectedRecord.value = new Error(
|
||||
`Could not inspect element with id "${element.id}". No renderer found.`,
|
||||
);
|
||||
|
||||
map.set(element, record);
|
||||
|
||||
@@ -139,7 +141,7 @@ export function inspectElement(
|
||||
|
||||
const rejectedRecord = ((newRecord: any): RejectedRecord);
|
||||
rejectedRecord.status = Rejected;
|
||||
rejectedRecord.value = `Could not inspect element with id "${element.id}". Error thrown:\n${error.message}`;
|
||||
rejectedRecord.value = error;
|
||||
|
||||
wake();
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user