Skip to content

Commit e531a4a

Browse files
authored
[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
1 parent 547b707 commit e531a4a

File tree

10 files changed

+201
-13
lines changed

10 files changed

+201
-13
lines changed

packages/react-devtools-shared/src/__tests__/inspectedElement-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2145,7 +2145,7 @@ describe('InspectedElement', () => {
21452145
expect(value).toBe(null);
21462146

21472147
const error = errorBoundaryInstance.state.error;
2148-
expect(error.message).toBe('Error rendering inspected component');
2148+
expect(error.message).toBe('Expected');
21492149
expect(error.stack).toContain('inspectHooksOfFiber');
21502150
});
21512151

packages/react-devtools-shared/src/backend/renderer.js

+48
Original file line numberDiff line numberDiff line change
@@ -3607,10 +3607,58 @@ export function attach(
36073607
try {
36083608
mostRecentlyInspectedElement = inspectElementRaw(id);
36093609
} 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
36103657
console.error('Error inspecting element.\n\n', error);
36113658

36123659
return {
36133660
type: 'error',
3661+
errorType: 'uncaught',
36143662
id,
36153663
responseID: requestID,
36163664
message: error.message,

packages/react-devtools-shared/src/backend/types.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -289,8 +289,9 @@ export type InspectElementError = {|
289289
id: number,
290290
responseID: number,
291291
type: 'error',
292+
errorType: 'user' | 'unknown-hook' | 'uncaught',
292293
message: string,
293-
stack: string,
294+
stack?: string,
294295
|};
295296

296297
export type InspectElementFullData = {|

packages/react-devtools-shared/src/backendAPI.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import {hydrate, fillInPath} from 'react-devtools-shared/src/hydration';
1111
import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
1212
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';
1414

1515
import type {
1616
InspectedElement as InspectedElementBackend,
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ import ErrorView from './ErrorView';
1515
import SearchingGitHubIssues from './SearchingGitHubIssues';
1616
import SuspendingErrorView from './SuspendingErrorView';
1717
import TimeoutView from './TimeoutView';
18+
import CaughtErrorView from './CaughtErrorView';
1819
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';
2023
import {logEvent} from 'react-devtools-shared/src/Logger';
2124

2225
type Props = {|
@@ -34,6 +37,8 @@ type State = {|
3437
hasError: boolean,
3538
isUnsupportedBridgeOperationError: boolean,
3639
isTimeout: boolean,
40+
isUserError: boolean,
41+
isUnknownHookError: boolean,
3742
|};
3843

3944
const InitialState: State = {
@@ -44,6 +49,8 @@ const InitialState: State = {
4449
hasError: false,
4550
isUnsupportedBridgeOperationError: false,
4651
isTimeout: false,
52+
isUserError: false,
53+
isUnknownHookError: false,
4754
};
4855

4956
export default class ErrorBoundary extends Component<Props, State> {
@@ -58,6 +65,8 @@ export default class ErrorBoundary extends Component<Props, State> {
5865
: null;
5966

6067
const isTimeout = error instanceof TimeoutError;
68+
const isUserError = error instanceof UserError;
69+
const isUnknownHookError = error instanceof UnknownHookError;
6170
const isUnsupportedBridgeOperationError =
6271
error instanceof UnsupportedBridgeOperationError;
6372

@@ -76,7 +85,9 @@ export default class ErrorBoundary extends Component<Props, State> {
7685
errorMessage,
7786
hasError: true,
7887
isUnsupportedBridgeOperationError,
88+
isUnknownHookError,
7989
isTimeout,
90+
isUserError,
8091
};
8192
}
8293

@@ -111,6 +122,8 @@ export default class ErrorBoundary extends Component<Props, State> {
111122
hasError,
112123
isUnsupportedBridgeOperationError,
113124
isTimeout,
125+
isUserError,
126+
isUnknownHookError,
114127
} = this.state;
115128

116129
if (hasError) {
@@ -133,6 +146,37 @@ export default class ErrorBoundary extends Component<Props, State> {
133146
errorMessage={errorMessage}
134147
/>
135148
);
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+
);
136180
} else {
137181
return (
138182
<ErrorView
@@ -141,10 +185,7 @@ export default class ErrorBoundary extends Component<Props, State> {
141185
dismissError={
142186
canDismissProp || canDismissState ? this._dismissError : null
143187
}
144-
errorMessage={errorMessage}
145-
isUnsupportedBridgeOperationError={
146-
isUnsupportedBridgeOperationError
147-
}>
188+
errorMessage={errorMessage}>
148189
<Suspense fallback={<SearchingGitHubIssues />}>
149190
<SuspendingErrorView
150191
callStack={callStack}
Original file line numberDiff line numberDiff line change
@@ -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+
}
Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import type {
2727
InspectedElement as InspectedElementFrontend,
2828
InspectedElementResponseType,
2929
} 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';
3032

3133
// Maps element ID to inspected data.
3234
// 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({
8082

8183
let inspectedElement;
8284
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+
}
8698
// 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;
89100

90101
throw error;
102+
}
91103

92104
case 'no-change':
93105
// This is a no-op for the purposes of our cache.

0 commit comments

Comments
 (0)