Skip to content

Commit

Permalink
Make onUncaughtError and onCaughtError Configurable (facebook#28641)
Browse files Browse the repository at this point in the history
Stacked on facebook#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.
  • Loading branch information
sebmarkbage authored and AndyPengc12 committed Apr 15, 2024
1 parent 08e45da commit 6cf1acf
Show file tree
Hide file tree
Showing 25 changed files with 1,115 additions and 275 deletions.
5 changes: 2 additions & 3 deletions packages/react-dom/index.classic.fb.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@ Object.assign((Internals: any), {

export {
createPortal,
createRoot,
hydrateRoot,
findDOMNode,
flushSync,
render,
unmountComponentAtNode,
unstable_batchedUpdates,
unstable_createEventHandle,
Expand All @@ -41,4 +38,6 @@ export {
version,
} from './src/client/ReactDOM';

export {createRoot, hydrateRoot, render} from './src/client/ReactDOMRootFB';

export {Internals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED};
4 changes: 2 additions & 2 deletions packages/react-dom/index.modern.fb.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
export {default as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED} from './src/ReactDOMSharedInternals';
export {
createPortal,
createRoot,
hydrateRoot,
flushSync,
unstable_batchedUpdates,
unstable_createEventHandle,
Expand All @@ -26,3 +24,5 @@ export {
preinitModule,
version,
} from './src/client/ReactDOM';

export {createRoot, hydrateRoot} from './src/client/ReactDOMRootFB';
2 changes: 2 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMRoot-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe('ReactDOMRoot', () => {
expect(container.textContent).toEqual('Hi');
});

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

// @gate !classic || !__DEV__
it('warns if you import hydrateRoot from react-dom', async () => {
expect(() => ReactDOM.hydrateRoot(container, null)).toErrorDev(
'You are importing hydrateRoot from "react-dom" which is not supported. ' +
Expand Down
8 changes: 7 additions & 1 deletion packages/react-dom/src/client/ReactDOMLegacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import {
getPublicRootInstance,
findHostInstance,
findHostInstanceWithWarning,
defaultOnUncaughtError,
defaultOnCaughtError,
} from 'react-reconciler/src/ReactFiberReconciler';
import {LegacyRoot} from 'react-reconciler/src/ReactRootTags';
import getComponentNameFromType from 'shared/getComponentNameFromType';
Expand Down Expand Up @@ -124,6 +126,8 @@ function legacyCreateRootFromDOMContainer(
false, // isStrictMode
false, // concurrentUpdatesByDefaultOverride,
'', // identifierPrefix
defaultOnUncaughtError,
defaultOnCaughtError,
noopOnRecoverableError,
// TODO(luna) Support hydration later
null,
Expand Down Expand Up @@ -158,7 +162,9 @@ function legacyCreateRootFromDOMContainer(
false, // isStrictMode
false, // concurrentUpdatesByDefaultOverride,
'', // identifierPrefix
noopOnRecoverableError, // onRecoverableError
defaultOnUncaughtError,
defaultOnCaughtError,
noopOnRecoverableError,
null, // transitionCallbacks
);
container._reactRootContainer = root;
Expand Down
61 changes: 53 additions & 8 deletions packages/react-dom/src/client/ReactDOMRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,21 @@ export type CreateRootOptions = {
unstable_concurrentUpdatesByDefault?: boolean,
unstable_transitionCallbacks?: TransitionTracingCallbacks,
identifierPrefix?: string,
onRecoverableError?: (error: mixed) => void,
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,
};

export type HydrateRootOptions = {
Expand All @@ -44,7 +58,21 @@ export type HydrateRootOptions = {
unstable_concurrentUpdatesByDefault?: boolean,
unstable_transitionCallbacks?: TransitionTracingCallbacks,
identifierPrefix?: string,
onRecoverableError?: (error: mixed) => void,
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,
formState?: ReactFormState<any, any> | null,
};

Expand All @@ -67,15 +95,12 @@ import {
updateContainer,
flushSync,
isAlreadyRendering,
defaultOnUncaughtError,
defaultOnCaughtError,
defaultOnRecoverableError,
} from 'react-reconciler/src/ReactFiberReconciler';
import {ConcurrentRoot} from 'react-reconciler/src/ReactRootTags';

import reportGlobalError from 'shared/reportGlobalError';

function defaultOnRecoverableError(error: mixed, errorInfo: any) {
reportGlobalError(error);
}

// $FlowFixMe[missing-this-annot]
function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
Expand Down Expand Up @@ -156,6 +181,8 @@ export function createRoot(
let isStrictMode = false;
let concurrentUpdatesByDefaultOverride = false;
let identifierPrefix = '';
let onUncaughtError = defaultOnUncaughtError;
let onCaughtError = defaultOnCaughtError;
let onRecoverableError = defaultOnRecoverableError;
let transitionCallbacks = null;

Expand Down Expand Up @@ -193,6 +220,12 @@ export function createRoot(
if (options.identifierPrefix !== undefined) {
identifierPrefix = options.identifierPrefix;
}
if (options.onUncaughtError !== undefined) {
onUncaughtError = options.onUncaughtError;
}
if (options.onCaughtError !== undefined) {
onCaughtError = options.onCaughtError;
}
if (options.onRecoverableError !== undefined) {
onRecoverableError = options.onRecoverableError;
}
Expand All @@ -208,6 +241,8 @@ export function createRoot(
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onUncaughtError,
onCaughtError,
onRecoverableError,
transitionCallbacks,
);
Expand Down Expand Up @@ -262,6 +297,8 @@ export function hydrateRoot(
let isStrictMode = false;
let concurrentUpdatesByDefaultOverride = false;
let identifierPrefix = '';
let onUncaughtError = defaultOnUncaughtError;
let onCaughtError = defaultOnCaughtError;
let onRecoverableError = defaultOnRecoverableError;
let transitionCallbacks = null;
let formState = null;
Expand All @@ -278,6 +315,12 @@ export function hydrateRoot(
if (options.identifierPrefix !== undefined) {
identifierPrefix = options.identifierPrefix;
}
if (options.onUncaughtError !== undefined) {
onUncaughtError = options.onUncaughtError;
}
if (options.onCaughtError !== undefined) {
onCaughtError = options.onCaughtError;
}
if (options.onRecoverableError !== undefined) {
onRecoverableError = options.onRecoverableError;
}
Expand All @@ -300,6 +343,8 @@ export function hydrateRoot(
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onUncaughtError,
onCaughtError,
onRecoverableError,
transitionCallbacks,
formState,
Expand Down
Loading

0 comments on commit 6cf1acf

Please sign in to comment.