Skip to content

Commit a053716

Browse files
authored
Make onUncaughtError and onCaughtError Configurable (#28641)
Stacked on #28627. This makes error logging configurable using these `createRoot`/`hydrateRoot` options: ``` onUncaughtError(error: mixed, errorInfo: {componentStack?: ?string}) => void onCaughtError(error: mixed, errorInfo: {componentStack?: ?string, errorBoundary?: ?React.Component<any, any>}) => void onRecoverableError(error: mixed, errorInfo: {digest?: ?string, componentStack?: ?string}) => void ``` We already have the `onRecoverableError` option since before. Overriding these can be used to implement custom error dialogs (with access to the `componentStack`). It can also be used to silence caught errors when testing an error boundary or if you prefer not getting logs for caught errors that you've already handled in an error boundary. I currently expose the error boundary instance but I think we should probably remove that since it doesn't make sense for non-class error boundaries and isn't very useful anyway. It's also unclear what it should do when an error is rethrown from one boundary to another. Since these are public APIs now we can implement the ReactFiberErrorDialog forks using these options at the roots of the builds. So I unforked those files and instead passed a custom option for the native and www builds. To do this I had to fork the ReactDOMLegacy file into ReactDOMRootFB which is a duplication but that will go away as soon as the FB fork is the only legacy root.
1 parent 9f8daa6 commit a053716

25 files changed

+1115
-275
lines changed

packages/react-dom/index.classic.fb.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,8 @@ Object.assign((Internals: any), {
2020

2121
export {
2222
createPortal,
23-
createRoot,
24-
hydrateRoot,
2523
findDOMNode,
2624
flushSync,
27-
render,
2825
unmountComponentAtNode,
2926
unstable_batchedUpdates,
3027
unstable_createEventHandle,
@@ -41,4 +38,6 @@ export {
4138
version,
4239
} from './src/client/ReactDOM';
4340

41+
export {createRoot, hydrateRoot, render} from './src/client/ReactDOMRootFB';
42+
4443
export {Internals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED};

packages/react-dom/index.modern.fb.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
export {default as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED} from './src/ReactDOMSharedInternals';
1111
export {
1212
createPortal,
13-
createRoot,
14-
hydrateRoot,
1513
flushSync,
1614
unstable_batchedUpdates,
1715
unstable_createEventHandle,
@@ -26,3 +24,5 @@ export {
2624
preinitModule,
2725
version,
2826
} from './src/client/ReactDOM';
27+
28+
export {createRoot, hydrateRoot} from './src/client/ReactDOMRootFB';

packages/react-dom/src/__tests__/ReactDOMRoot-test.js

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ describe('ReactDOMRoot', () => {
4747
expect(container.textContent).toEqual('Hi');
4848
});
4949

50+
// @gate !classic || !__DEV__
5051
it('warns if you import createRoot from react-dom', async () => {
5152
expect(() => ReactDOM.createRoot(container)).toErrorDev(
5253
'You are importing createRoot from "react-dom" which is not supported. ' +
@@ -57,6 +58,7 @@ describe('ReactDOMRoot', () => {
5758
);
5859
});
5960

61+
// @gate !classic || !__DEV__
6062
it('warns if you import hydrateRoot from react-dom', async () => {
6163
expect(() => ReactDOM.hydrateRoot(container, null)).toErrorDev(
6264
'You are importing hydrateRoot from "react-dom" which is not supported. ' +

packages/react-dom/src/client/ReactDOMLegacy.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import {
3939
getPublicRootInstance,
4040
findHostInstance,
4141
findHostInstanceWithWarning,
42+
defaultOnUncaughtError,
43+
defaultOnCaughtError,
4244
} from 'react-reconciler/src/ReactFiberReconciler';
4345
import {LegacyRoot} from 'react-reconciler/src/ReactRootTags';
4446
import getComponentNameFromType from 'shared/getComponentNameFromType';
@@ -124,6 +126,8 @@ function legacyCreateRootFromDOMContainer(
124126
false, // isStrictMode
125127
false, // concurrentUpdatesByDefaultOverride,
126128
'', // identifierPrefix
129+
defaultOnUncaughtError,
130+
defaultOnCaughtError,
127131
noopOnRecoverableError,
128132
// TODO(luna) Support hydration later
129133
null,
@@ -158,7 +162,9 @@ function legacyCreateRootFromDOMContainer(
158162
false, // isStrictMode
159163
false, // concurrentUpdatesByDefaultOverride,
160164
'', // identifierPrefix
161-
noopOnRecoverableError, // onRecoverableError
165+
defaultOnUncaughtError,
166+
defaultOnCaughtError,
167+
noopOnRecoverableError,
162168
null, // transitionCallbacks
163169
);
164170
container._reactRootContainer = root;

packages/react-dom/src/client/ReactDOMRoot.js

+53-8
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,21 @@ export type CreateRootOptions = {
3232
unstable_concurrentUpdatesByDefault?: boolean,
3333
unstable_transitionCallbacks?: TransitionTracingCallbacks,
3434
identifierPrefix?: string,
35-
onRecoverableError?: (error: mixed) => void,
35+
onUncaughtError?: (
36+
error: mixed,
37+
errorInfo: {+componentStack?: ?string},
38+
) => void,
39+
onCaughtError?: (
40+
error: mixed,
41+
errorInfo: {
42+
+componentStack?: ?string,
43+
+errorBoundary?: ?React$Component<any, any>,
44+
},
45+
) => void,
46+
onRecoverableError?: (
47+
error: mixed,
48+
errorInfo: {+digest?: ?string, +componentStack?: ?string},
49+
) => void,
3650
};
3751

3852
export type HydrateRootOptions = {
@@ -44,7 +58,21 @@ export type HydrateRootOptions = {
4458
unstable_concurrentUpdatesByDefault?: boolean,
4559
unstable_transitionCallbacks?: TransitionTracingCallbacks,
4660
identifierPrefix?: string,
47-
onRecoverableError?: (error: mixed) => void,
61+
onUncaughtError?: (
62+
error: mixed,
63+
errorInfo: {+componentStack?: ?string},
64+
) => void,
65+
onCaughtError?: (
66+
error: mixed,
67+
errorInfo: {
68+
+componentStack?: ?string,
69+
+errorBoundary?: ?React$Component<any, any>,
70+
},
71+
) => void,
72+
onRecoverableError?: (
73+
error: mixed,
74+
errorInfo: {+digest?: ?string, +componentStack?: ?string},
75+
) => void,
4876
formState?: ReactFormState<any, any> | null,
4977
};
5078

@@ -67,15 +95,12 @@ import {
6795
updateContainer,
6896
flushSync,
6997
isAlreadyRendering,
98+
defaultOnUncaughtError,
99+
defaultOnCaughtError,
100+
defaultOnRecoverableError,
70101
} from 'react-reconciler/src/ReactFiberReconciler';
71102
import {ConcurrentRoot} from 'react-reconciler/src/ReactRootTags';
72103

73-
import reportGlobalError from 'shared/reportGlobalError';
74-
75-
function defaultOnRecoverableError(error: mixed, errorInfo: any) {
76-
reportGlobalError(error);
77-
}
78-
79104
// $FlowFixMe[missing-this-annot]
80105
function ReactDOMRoot(internalRoot: FiberRoot) {
81106
this._internalRoot = internalRoot;
@@ -156,6 +181,8 @@ export function createRoot(
156181
let isStrictMode = false;
157182
let concurrentUpdatesByDefaultOverride = false;
158183
let identifierPrefix = '';
184+
let onUncaughtError = defaultOnUncaughtError;
185+
let onCaughtError = defaultOnCaughtError;
159186
let onRecoverableError = defaultOnRecoverableError;
160187
let transitionCallbacks = null;
161188

@@ -193,6 +220,12 @@ export function createRoot(
193220
if (options.identifierPrefix !== undefined) {
194221
identifierPrefix = options.identifierPrefix;
195222
}
223+
if (options.onUncaughtError !== undefined) {
224+
onUncaughtError = options.onUncaughtError;
225+
}
226+
if (options.onCaughtError !== undefined) {
227+
onCaughtError = options.onCaughtError;
228+
}
196229
if (options.onRecoverableError !== undefined) {
197230
onRecoverableError = options.onRecoverableError;
198231
}
@@ -208,6 +241,8 @@ export function createRoot(
208241
isStrictMode,
209242
concurrentUpdatesByDefaultOverride,
210243
identifierPrefix,
244+
onUncaughtError,
245+
onCaughtError,
211246
onRecoverableError,
212247
transitionCallbacks,
213248
);
@@ -262,6 +297,8 @@ export function hydrateRoot(
262297
let isStrictMode = false;
263298
let concurrentUpdatesByDefaultOverride = false;
264299
let identifierPrefix = '';
300+
let onUncaughtError = defaultOnUncaughtError;
301+
let onCaughtError = defaultOnCaughtError;
265302
let onRecoverableError = defaultOnRecoverableError;
266303
let transitionCallbacks = null;
267304
let formState = null;
@@ -278,6 +315,12 @@ export function hydrateRoot(
278315
if (options.identifierPrefix !== undefined) {
279316
identifierPrefix = options.identifierPrefix;
280317
}
318+
if (options.onUncaughtError !== undefined) {
319+
onUncaughtError = options.onUncaughtError;
320+
}
321+
if (options.onCaughtError !== undefined) {
322+
onCaughtError = options.onCaughtError;
323+
}
281324
if (options.onRecoverableError !== undefined) {
282325
onRecoverableError = options.onRecoverableError;
283326
}
@@ -300,6 +343,8 @@ export function hydrateRoot(
300343
isStrictMode,
301344
concurrentUpdatesByDefaultOverride,
302345
identifierPrefix,
346+
onUncaughtError,
347+
onCaughtError,
303348
onRecoverableError,
304349
transitionCallbacks,
305350
formState,

0 commit comments

Comments
 (0)