From e531a4a62d0b88301ac06d4efd3f5a30faa03c94 Mon Sep 17 00:00:00 2001 From: "Mengdi \"Monday\" Chen" Date: Thu, 5 May 2022 20:17:23 -0400 Subject: [PATCH] [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 --- .../src/__tests__/inspectedElement-test.js | 2 +- .../src/backend/renderer.js | 48 +++++++++++++++++ .../src/backend/types.js | 3 +- .../react-devtools-shared/src/backendAPI.js | 2 +- .../views/ErrorBoundary/CaughtErrorView.js | 44 ++++++++++++++++ .../views/ErrorBoundary/ErrorBoundary.js | 51 +++++++++++++++++-- .../src/{ => errors}/TimeoutError.js | 0 .../src/errors/UnknownHookError.js | 21 ++++++++ .../src/errors/UserError.js | 21 ++++++++ .../src/inspectedElementMutableSource.js | 22 ++++++-- 10 files changed, 201 insertions(+), 13 deletions(-) create mode 100644 packages/react-devtools-shared/src/devtools/views/ErrorBoundary/CaughtErrorView.js rename packages/react-devtools-shared/src/{ => errors}/TimeoutError.js (100%) create mode 100644 packages/react-devtools-shared/src/errors/UnknownHookError.js create mode 100644 packages/react-devtools-shared/src/errors/UserError.js diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js index 81c0a3a758212..919fbb059f76b 100644 --- a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js @@ -2145,7 +2145,7 @@ describe('InspectedElement', () => { expect(value).toBe(null); const error = errorBoundaryInstance.state.error; - expect(error.message).toBe('Error rendering inspected component'); + expect(error.message).toBe('Expected'); expect(error.stack).toContain('inspectHooksOfFiber'); }); diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index c161902df711c..5e09a2d0f8043 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -3607,10 +3607,58 @@ export function attach( try { mostRecentlyInspectedElement = inspectElementRaw(id); } catch (error) { + // the error name is synced with ReactDebugHooks + if (error.name === 'ReactDebugToolsRenderError') { + let message = 'Error rendering inspected element.'; + let stack; + // Log error & cause for user to debug + console.error(message + '\n\n', error); + if (error.cause != null) { + const fiber = findCurrentFiberUsingSlowPathById(id); + const componentName = + fiber != null ? getDisplayNameForFiber(fiber) : null; + console.error( + 'React DevTools encountered an error while trying to inspect hooks. ' + + 'This is most likely caused by an error in current inspected component' + + (componentName != null ? `: "${componentName}".` : '.') + + '\nThe error thrown in the component is: \n\n', + error.cause, + ); + if (error.cause instanceof Error) { + message = error.cause.message || message; + stack = error.cause.stack; + } + } + + return { + type: 'error', + errorType: 'user', + id, + responseID: requestID, + message, + stack, + }; + } + + // the error name is synced with ReactDebugHooks + if (error.name === 'ReactDebugToolsUnsupportedHookError') { + return { + type: 'error', + errorType: 'unknown-hook', + id, + responseID: requestID, + message: + 'Unsupported hook in the react-debug-tools package: ' + + error.message, + }; + } + + // Log Uncaught Error console.error('Error inspecting element.\n\n', error); return { type: 'error', + errorType: 'uncaught', id, responseID: requestID, message: error.message, diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 4d975dbfec0d5..92ad42c3010cf 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -289,8 +289,9 @@ export type InspectElementError = {| id: number, responseID: number, type: 'error', + errorType: 'user' | 'unknown-hook' | 'uncaught', message: string, - stack: string, + stack?: string, |}; export type InspectElementFullData = {| diff --git a/packages/react-devtools-shared/src/backendAPI.js b/packages/react-devtools-shared/src/backendAPI.js index 3849899b7df02..adf0c5e8b0911 100644 --- a/packages/react-devtools-shared/src/backendAPI.js +++ b/packages/react-devtools-shared/src/backendAPI.js @@ -10,7 +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 TimeoutError from 'react-devtools-shared/src/errors/TimeoutError'; import type { InspectedElement as InspectedElementBackend, diff --git a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/CaughtErrorView.js b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/CaughtErrorView.js new file mode 100644 index 0000000000000..c4511ba0312bd --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/CaughtErrorView.js @@ -0,0 +1,44 @@ +/** + * 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 styles from './shared.css'; + +type Props = {| + callStack: string | null, + children: React$Node, + info: React$Node | null, + componentStack: string | null, + errorMessage: string, +|}; + +export default function CaughtErrorView({ + callStack, + children, + info, + componentStack, + errorMessage, +}: Props) { + return ( +
+ {children} +
+
+
{errorMessage}
+
+ {!!info &&
{info}
} + {!!callStack && ( +
+ The error was thrown {callStack.trim()} +
+ )} +
+
+ ); +} diff --git a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js index f4994e23a9bf5..d207fcfe4f736 100644 --- a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js +++ b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js @@ -15,8 +15,11 @@ import ErrorView from './ErrorView'; import SearchingGitHubIssues from './SearchingGitHubIssues'; import SuspendingErrorView from './SuspendingErrorView'; import TimeoutView from './TimeoutView'; +import CaughtErrorView from './CaughtErrorView'; import UnsupportedBridgeOperationError from 'react-devtools-shared/src/UnsupportedBridgeOperationError'; -import TimeoutError from 'react-devtools-shared/src/TimeoutError'; +import TimeoutError from 'react-devtools-shared/src/errors/TimeoutError'; +import UserError from 'react-devtools-shared/src/errors/UserError'; +import UnknownHookError from 'react-devtools-shared/src/errors/UnknownHookError'; import {logEvent} from 'react-devtools-shared/src/Logger'; type Props = {| @@ -34,6 +37,8 @@ type State = {| hasError: boolean, isUnsupportedBridgeOperationError: boolean, isTimeout: boolean, + isUserError: boolean, + isUnknownHookError: boolean, |}; const InitialState: State = { @@ -44,6 +49,8 @@ const InitialState: State = { hasError: false, isUnsupportedBridgeOperationError: false, isTimeout: false, + isUserError: false, + isUnknownHookError: false, }; export default class ErrorBoundary extends Component { @@ -58,6 +65,8 @@ export default class ErrorBoundary extends Component { : null; const isTimeout = error instanceof TimeoutError; + const isUserError = error instanceof UserError; + const isUnknownHookError = error instanceof UnknownHookError; const isUnsupportedBridgeOperationError = error instanceof UnsupportedBridgeOperationError; @@ -76,7 +85,9 @@ export default class ErrorBoundary extends Component { errorMessage, hasError: true, isUnsupportedBridgeOperationError, + isUnknownHookError, isTimeout, + isUserError, }; } @@ -111,6 +122,8 @@ export default class ErrorBoundary extends Component { hasError, isUnsupportedBridgeOperationError, isTimeout, + isUserError, + isUnknownHookError, } = this.state; if (hasError) { @@ -133,6 +146,37 @@ export default class ErrorBoundary extends Component { errorMessage={errorMessage} /> ); + } else if (isUserError) { + return ( + + React DevTools encountered an error while trying to inspect the + hooks. This is most likely caused by a developer error in the + currently inspected element. Please see your console for logged + error. + + } + /> + ); + } else if (isUnknownHookError) { + return ( + + React DevTools encountered an unknown hook. This is probably + because the react-debug-tools package is out of date. To fix, + upgrade the React DevTools to the most recent version. + + } + /> + ); } else { return ( { dismissError={ canDismissProp || canDismissState ? this._dismissError : null } - errorMessage={errorMessage} - isUnsupportedBridgeOperationError={ - isUnsupportedBridgeOperationError - }> + errorMessage={errorMessage}> }>