@samitouri / QOS-React-2 / commits / e531a4a62d

[React DevTools] Improve DevTools UI when Inspecting a user Component that Throws an Error (#24248)

* [ReactDevTools] custom view for errors occur in user's code * [ReactDevTools] show message for unsupported feature * fix bad import * fix typo * fix issues from rebasing * prettier * sync error names * sync error name with upstream * fix lint & better comment * fix error message for test * better error message per review * add missing file * remove dead enum & provide component name in error message * better error message * better user facing error message

Mengdi "Monday" Chen committed May 5, 2022 at 20:17 UTC e531a4a62d0b88301ac06d4efd3f5a30faa03c94
10 files changed +201 -13
packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
+1 -1
@@ -2145,7 +2145,7 @@ describe('InspectedElement', () => {
2145 expect(value).toBe(null);
2146
2147 const error = errorBoundaryInstance.state.error;
2148 - expect(error.message).toBe('Error rendering inspected component');
2148 + expect(error.message).toBe('Expected');
2149 expect(error.stack).toContain('inspectHooksOfFiber');
2150 });
2151
packages/react-devtools-shared/src/backend/renderer.js
+48
@@ -3607,10 +3607,58 @@ export function attach(
3607 try {
3608 mostRecentlyInspectedElement = inspectElementRaw(id);
3609 } catch (error) {
3610 + // the error name is synced with ReactDebugHooks
3611 + if (error.name === 'ReactDebugToolsRenderError') {
3612 + let message = 'Error rendering inspected element.';
3613 + let stack;
3614 + // Log error & cause for user to debug
3615 + console.error(message + '\n\n', error);
3616 + if (error.cause != null) {
3617 + const fiber = findCurrentFiberUsingSlowPathById(id);
3618 + const componentName =
3619 + fiber != null ? getDisplayNameForFiber(fiber) : null;
3620 + console.error(
3621 + 'React DevTools encountered an error while trying to inspect hooks. ' +
3622 + 'This is most likely caused by an error in current inspected component' +
3623 + (componentName != null ? `: "${componentName}".` : '.') +
3624 + '\nThe error thrown in the component is: \n\n',
3625 + error.cause,
3626 + );
3627 + if (error.cause instanceof Error) {
3628 + message = error.cause.message || message;
3629 + stack = error.cause.stack;
3630 + }
3631 + }
3632 +
3633 + return {
3634 + type: 'error',
3635 + errorType: 'user',
3636 + id,
3637 + responseID: requestID,
3638 + message,
3639 + stack,
3640 + };
3641 + }
3642 +
3643 + // the error name is synced with ReactDebugHooks
3644 + if (error.name === 'ReactDebugToolsUnsupportedHookError') {
3645 + return {
3646 + type: 'error',
3647 + errorType: 'unknown-hook',
3648 + id,
3649 + responseID: requestID,
3650 + message:
3651 + 'Unsupported hook in the react-debug-tools package: ' +
3652 + error.message,
3653 + };
3654 + }
3655 +
3656 + // Log Uncaught Error
3657 console.error('Error inspecting element.\n\n', error);
3658
3659 return {
3660 type: 'error',
3661 + errorType: 'uncaught',
3662 id,
3663 responseID: requestID,
3664 message: error.message,
packages/react-devtools-shared/src/backend/types.js
+2 -1
@@ -289,8 +289,9 @@ export type InspectElementError = {|
289 id: number,
290 responseID: number,
291 type: 'error',
292 + errorType: 'user' | 'unknown-hook' | 'uncaught',
293 message: string,
293 - stack: string,
294 + stack?: string,
295 |};
296
297 export type InspectElementFullData = {|
packages/react-devtools-shared/src/backendAPI.js
+1 -1
@@ -10,7 +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';
13 +import TimeoutError from 'react-devtools-shared/src/errors/TimeoutError';
14
15 import type {
16 InspectedElement as InspectedElementBackend,
packages/react-devtools-shared/src/devtools/views/ErrorBoundary/CaughtErrorView.js new
+44
@@ -0,0 +1,44 @@
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 styles from './shared.css';
12 +
13 +type Props = {|
14 + callStack: string | null,
15 + children: React$Node,
16 + info: React$Node | null,
17 + componentStack: string | null,
18 + errorMessage: string,
19 +|};
20 +
21 +export default function CaughtErrorView({
22 + callStack,
23 + children,
24 + info,
25 + componentStack,
26 + errorMessage,
27 +}: Props) {
28 + return (
29 + <div className={styles.ErrorBoundary}>
30 + {children}
31 + <div className={styles.ErrorInfo}>
32 + <div className={styles.HeaderRow}>
33 + <div className={styles.ErrorHeader}>{errorMessage}</div>
34 + </div>
35 + {!!info && <div className={styles.InfoBox}>{info}</div>}
36 + {!!callStack && (
37 + <div className={styles.ErrorStack}>
38 + The error was thrown {callStack.trim()}
39 + </div>
40 + )}
41 + </div>
42 + </div>
43 + );
44 +}
packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js
+46 -5
@@ -15,8 +15,11 @@ import ErrorView from './ErrorView';
15 import SearchingGitHubIssues from './SearchingGitHubIssues';
16 import SuspendingErrorView from './SuspendingErrorView';
17 import TimeoutView from './TimeoutView';
18 +import CaughtErrorView from './CaughtErrorView';
19 import UnsupportedBridgeOperationError from 'react-devtools-shared/src/UnsupportedBridgeOperationError';
19 -import TimeoutError from 'react-devtools-shared/src/TimeoutError';
20 +import TimeoutError from 'react-devtools-shared/src/errors/TimeoutError';
21 +import UserError from 'react-devtools-shared/src/errors/UserError';
22 +import UnknownHookError from 'react-devtools-shared/src/errors/UnknownHookError';
23 import {logEvent} from 'react-devtools-shared/src/Logger';
24
25 type Props = {|
@@ -34,6 +37,8 @@ type State = {|
37 hasError: boolean,
38 isUnsupportedBridgeOperationError: boolean,
39 isTimeout: boolean,
40 + isUserError: boolean,
41 + isUnknownHookError: boolean,
42 |};
43
44 const InitialState: State = {
@@ -44,6 +49,8 @@ const InitialState: State = {
49 hasError: false,
50 isUnsupportedBridgeOperationError: false,
51 isTimeout: false,
52 + isUserError: false,
53 + isUnknownHookError: false,
54 };
55
56 export default class ErrorBoundary extends Component<Props, State> {
@@ -58,6 +65,8 @@ export default class ErrorBoundary extends Component<Props, State> {
65 : null;
66
67 const isTimeout = error instanceof TimeoutError;
68 + const isUserError = error instanceof UserError;
69 + const isUnknownHookError = error instanceof UnknownHookError;
70 const isUnsupportedBridgeOperationError =
71 error instanceof UnsupportedBridgeOperationError;
72
@@ -76,7 +85,9 @@ export default class ErrorBoundary extends Component<Props, State> {
85 errorMessage,
86 hasError: true,
87 isUnsupportedBridgeOperationError,
88 + isUnknownHookError,
89 isTimeout,
90 + isUserError,
91 };
92 }
93
@@ -111,6 +122,8 @@ export default class ErrorBoundary extends Component<Props, State> {
122 hasError,
123 isUnsupportedBridgeOperationError,
124 isTimeout,
125 + isUserError,
126 + isUnknownHookError,
127 } = this.state;
128
129 if (hasError) {
@@ -133,6 +146,37 @@ export default class ErrorBoundary extends Component<Props, State> {
146 errorMessage={errorMessage}
147 />
148 );
149 + } else if (isUserError) {
150 + return (
151 + <CaughtErrorView
152 + callStack={callStack}
153 + componentStack={componentStack}
154 + errorMessage={errorMessage || 'Error occured in inspected element'}
155 + info={
156 + <>
157 + React DevTools encountered an error while trying to inspect the
158 + hooks. This is most likely caused by a developer error in the
159 + currently inspected element. Please see your console for logged
160 + error.
161 + </>
162 + }
163 + />
164 + );
165 + } else if (isUnknownHookError) {
166 + return (
167 + <CaughtErrorView
168 + callStack={callStack}
169 + componentStack={componentStack}
170 + errorMessage={errorMessage || 'Encountered an unknown hook'}
171 + info={
172 + <>
173 + React DevTools encountered an unknown hook. This is probably
174 + because the react-debug-tools package is out of date. To fix,
175 + upgrade the React DevTools to the most recent version.
176 + </>
177 + }
178 + />
179 + );
180 } else {
181 return (
182 <ErrorView
@@ -141,10 +185,7 @@ export default class ErrorBoundary extends Component<Props, State> {
185 dismissError={
186 canDismissProp || canDismissState ? this._dismissError : null
187 }
144 - errorMessage={errorMessage}
145 - isUnsupportedBridgeOperationError={
146 - isUnsupportedBridgeOperationError
147 - }>
188 + errorMessage={errorMessage}>
189 <Suspense fallback={<SearchingGitHubIssues />}>
190 <SuspendingErrorView
191 callStack={callStack}
packages/react-devtools-shared/src/errors/TimeoutError.js renamed
packages/react-devtools-shared/src/errors/UnknownHookError.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 UnknownHookError 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, UnknownHookError);
17 + }
18 +
19 + this.name = 'UnknownHookError';
20 + }
21 +}
packages/react-devtools-shared/src/errors/UserError.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 UserError 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, UserError);
17 + }
18 +
19 + this.name = 'UserError';
20 + }
21 +}
packages/react-devtools-shared/src/inspectedElementMutableSource.js
+17 -5
@@ -27,6 +27,8 @@ import type {
27 InspectedElement as InspectedElementFrontend,
28 InspectedElementResponseType,
29 } from 'react-devtools-shared/src/devtools/views/Components/types';
30 +import UserError from 'react-devtools-shared/src/errors/UserError';
31 +import UnknownHookError from 'react-devtools-shared/src/errors/UnknownHookError';
32
33 // Maps element ID to inspected data.
34 // We use an LRU for this rather than a WeakMap because of how the "no-change" optimization works.
@@ -80,14 +82,24 @@ export function inspectElement({
82
83 let inspectedElement;
84 switch (type) {
83 - case 'error':
84 - const {message, stack} = ((data: any): InspectElementError);
85 -
85 + case 'error': {
86 + const {message, stack, errorType} = ((data: any): InspectElementError);
87 +
88 + // create a different error class for each error type
89 + // and keep useful information from backend.
90 + let error;
91 + if (errorType === 'user') {
92 + error = new UserError(message);
93 + } else if (errorType === 'unknown-hook') {
94 + error = new UnknownHookError(message);
95 + } else {
96 + error = new Error(message);
97 + }
98 // The backend's stack (where the error originated) is more meaningful than this stack.
87 - const error = new Error(message);
88 - error.stack = stack;
99 + error.stack = stack || error.stack;
100
101 throw error;
102 + }
103
104 case 'no-change':
105 // This is a no-op for the purposes of our cache.