main
js 225 lines 6.41 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and 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 {Component, Suspense} from 'react';
12 import Store from 'react-devtools-shared/src/devtools/store';
13 import UnsupportedBridgeOperationView from './UnsupportedBridgeOperationView';
14 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';
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 {logErrorEvent} from 'react-devtools-shared/src/Logger';
24
25 type Props = {
26 children: React$Node,
27 canDismiss?: boolean,
28 onBeforeDismissCallback?: () => void,
29 store?: Store,
30 };
31
32 type State = {
33 callStack: string | null,
34 canDismiss: boolean,
35 componentStack: string | null,
36 errorMessage: string | null,
37 hasError: boolean,
38 isUnsupportedBridgeOperationError: boolean,
39 isTimeout: boolean,
40 isUserError: boolean,
41 isUnknownHookError: boolean,
42 };
43
44 const InitialState: State = {
45 callStack: null,
46 canDismiss: false,
47 componentStack: null,
48 errorMessage: null,
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> {
57 state: State = InitialState;
58
59 static getDerivedStateFromError(error: any): {
60 callStack: string | null,
61 errorMessage: string | null,
62 hasError: boolean,
63 isTimeout: boolean,
64 isUnknownHookError: boolean,
65 isUnsupportedBridgeOperationError: boolean,
66 isUserError: boolean,
67 } {
68 const errorMessage =
69 typeof error === 'object' &&
70 error !== null &&
71 typeof error.message === 'string'
72 ? error.message
73 : null;
74
75 const isTimeout = error instanceof TimeoutError;
76 const isUserError = error instanceof UserError;
77 const isUnknownHookError = error instanceof UnknownHookError;
78 const isUnsupportedBridgeOperationError =
79 error instanceof UnsupportedBridgeOperationError;
80
81 const callStack =
82 typeof error === 'object' &&
83 error !== null &&
84 typeof error.stack === 'string'
85 ? error.stack.split('\n').slice(1).join('\n')
86 : null;
87
88 return {
89 callStack,
90 errorMessage,
91 hasError: true,
92 isUnsupportedBridgeOperationError,
93 isUnknownHookError,
94 isTimeout,
95 isUserError,
96 };
97 }
98
99 componentDidCatch(error: any, {componentStack}: any) {
100 // This is an error trown during render, not a Store error
101 logErrorEvent(error, componentStack);
102 this.setState({componentStack});
103 }
104
105 componentDidMount() {
106 const {store} = this.props;
107 if (store != null) {
108 store.addListener('error', this._onStoreError);
109 }
110 }
111
112 componentWillUnmount() {
113 const {store} = this.props;
114 if (store != null) {
115 store.removeListener('error', this._onStoreError);
116 }
117 }
118
119 render(): React.Node {
120 const {canDismiss: canDismissProp, children} = this.props;
121 const {
122 callStack,
123 canDismiss: canDismissState,
124 componentStack,
125 errorMessage,
126 hasError,
127 isUnsupportedBridgeOperationError,
128 isTimeout,
129 isUserError,
130 isUnknownHookError,
131 } = this.state;
132
133 if (hasError) {
134 if (isTimeout) {
135 return (
136 <TimeoutView
137 callStack={callStack}
138 componentStack={componentStack}
139 dismissError={
140 canDismissProp || canDismissState ? this._dismissError : null
141 }
142 errorMessage={errorMessage}
143 />
144 );
145 } else if (isUnsupportedBridgeOperationError) {
146 return (
147 <UnsupportedBridgeOperationView
148 callStack={callStack}
149 componentStack={componentStack}
150 errorMessage={errorMessage}
151 />
152 );
153 } else if (isUserError) {
154 return (
155 <CaughtErrorView
156 callStack={callStack}
157 componentStack={componentStack}
158 errorMessage={errorMessage || 'Error occurred in inspected element'}
159 info={
160 <>
161 React DevTools encountered an error while trying to inspect the
162 hooks. This is most likely caused by a developer error in the
163 currently inspected element. Please see your console for logged
164 error.
165 </>
166 }
167 />
168 );
169 } else if (isUnknownHookError) {
170 return (
171 <CaughtErrorView
172 callStack={callStack}
173 componentStack={componentStack}
174 errorMessage={errorMessage || 'Encountered an unknown hook'}
175 info={
176 <>
177 React DevTools encountered an unknown hook. This is probably
178 because the react-debug-tools package is out of date. To fix,
179 upgrade the React DevTools to the most recent version.
180 </>
181 }
182 />
183 );
184 } else {
185 return (
186 <ErrorView
187 callStack={callStack}
188 componentStack={componentStack}
189 dismissError={
190 canDismissProp || canDismissState ? this._dismissError : null
191 }
192 errorMessage={errorMessage}>
193 <Suspense fallback={<SearchingGitHubIssues />}>
194 <SuspendingErrorView
195 callStack={callStack}
196 componentStack={componentStack}
197 errorMessage={errorMessage}
198 />
199 </Suspense>
200 </ErrorView>
201 );
202 }
203 }
204
205 return children;
206 }
207
208 _dismissError: () => void = () => {
209 const onBeforeDismissCallback = this.props.onBeforeDismissCallback;
210 if (typeof onBeforeDismissCallback === 'function') {
211 onBeforeDismissCallback();
212 }
213
214 this.setState(InitialState);
215 };
216
217 _onStoreError: (error: Error) => void = error => {
218 if (!this.state.hasError) {
219 this.setState({
220 ...ErrorBoundary.getDerivedStateFromError(error),
221 canDismiss: true,
222 });
223 }
224 };
225 }