mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
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.
This commit is contained in:
committed by
GitHub
parent
9f8daa6cb5
commit
a053716077
@@ -20,11 +20,8 @@ Object.assign((Internals: any), {
|
||||
|
||||
export {
|
||||
createPortal,
|
||||
createRoot,
|
||||
hydrateRoot,
|
||||
findDOMNode,
|
||||
flushSync,
|
||||
render,
|
||||
unmountComponentAtNode,
|
||||
unstable_batchedUpdates,
|
||||
unstable_createEventHandle,
|
||||
@@ -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};
|
||||
|
||||
@@ -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,
|
||||
@@ -26,3 +24,5 @@ export {
|
||||
preinitModule,
|
||||
version,
|
||||
} from './src/client/ReactDOM';
|
||||
|
||||
export {createRoot, hydrateRoot} from './src/client/ReactDOMRootFB';
|
||||
|
||||
@@ -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. ' +
|
||||
@@ -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. ' +
|
||||
|
||||
@@ -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';
|
||||
@@ -124,6 +126,8 @@ function legacyCreateRootFromDOMContainer(
|
||||
false, // isStrictMode
|
||||
false, // concurrentUpdatesByDefaultOverride,
|
||||
'', // identifierPrefix
|
||||
defaultOnUncaughtError,
|
||||
defaultOnCaughtError,
|
||||
noopOnRecoverableError,
|
||||
// TODO(luna) Support hydration later
|
||||
null,
|
||||
@@ -158,7 +162,9 @@ function legacyCreateRootFromDOMContainer(
|
||||
false, // isStrictMode
|
||||
false, // concurrentUpdatesByDefaultOverride,
|
||||
'', // identifierPrefix
|
||||
noopOnRecoverableError, // onRecoverableError
|
||||
defaultOnUncaughtError,
|
||||
defaultOnCaughtError,
|
||||
noopOnRecoverableError,
|
||||
null, // transitionCallbacks
|
||||
);
|
||||
container._reactRootContainer = root;
|
||||
|
||||
61
packages/react-dom/src/client/ReactDOMRoot.js
vendored
61
packages/react-dom/src/client/ReactDOMRoot.js
vendored
@@ -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 = {
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -208,6 +241,8 @@ export function createRoot(
|
||||
isStrictMode,
|
||||
concurrentUpdatesByDefaultOverride,
|
||||
identifierPrefix,
|
||||
onUncaughtError,
|
||||
onCaughtError,
|
||||
onRecoverableError,
|
||||
transitionCallbacks,
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -300,6 +343,8 @@ export function hydrateRoot(
|
||||
isStrictMode,
|
||||
concurrentUpdatesByDefaultOverride,
|
||||
identifierPrefix,
|
||||
onUncaughtError,
|
||||
onCaughtError,
|
||||
onRecoverableError,
|
||||
transitionCallbacks,
|
||||
formState,
|
||||
|
||||
418
packages/react-dom/src/client/ReactDOMRootFB.js
vendored
Normal file
418
packages/react-dom/src/client/ReactDOMRootFB.js
vendored
Normal file
@@ -0,0 +1,418 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and 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 type {ReactNodeList} from 'shared/ReactTypes';
|
||||
|
||||
import type {
|
||||
RootType,
|
||||
CreateRootOptions,
|
||||
HydrateRootOptions,
|
||||
} from './ReactDOMRoot';
|
||||
|
||||
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
|
||||
|
||||
import type {
|
||||
Container,
|
||||
PublicInstance,
|
||||
} from 'react-dom-bindings/src/client/ReactFiberConfigDOM';
|
||||
|
||||
import {
|
||||
createRoot as createRootImpl,
|
||||
hydrateRoot as hydrateRootImpl,
|
||||
} from './ReactDOMRoot';
|
||||
|
||||
import {disableLegacyMode} from 'shared/ReactFeatureFlags';
|
||||
import {clearContainer} from 'react-dom-bindings/src/client/ReactFiberConfigDOM';
|
||||
import {
|
||||
getInstanceFromNode,
|
||||
isContainerMarkedAsRoot,
|
||||
markContainerAsRoot,
|
||||
} from 'react-dom-bindings/src/client/ReactDOMComponentTree';
|
||||
import {listenToAllSupportedEvents} from 'react-dom-bindings/src/events/DOMPluginEventSystem';
|
||||
import {isValidContainerLegacy} from './ReactDOMRoot';
|
||||
import {
|
||||
DOCUMENT_NODE,
|
||||
COMMENT_NODE,
|
||||
} from 'react-dom-bindings/src/client/HTMLNodeType';
|
||||
|
||||
import {
|
||||
createContainer,
|
||||
createHydrationContainer,
|
||||
findHostInstanceWithNoPortals,
|
||||
updateContainer,
|
||||
flushSync,
|
||||
getPublicRootInstance,
|
||||
defaultOnUncaughtError,
|
||||
defaultOnCaughtError,
|
||||
} from 'react-reconciler/src/ReactFiberReconciler';
|
||||
import {LegacyRoot} from 'react-reconciler/src/ReactRootTags';
|
||||
import {has as hasInstance} from 'shared/ReactInstanceMap';
|
||||
|
||||
import assign from 'shared/assign';
|
||||
|
||||
// Provided by www
|
||||
const ReactFiberErrorDialogWWW = require('ReactFiberErrorDialog');
|
||||
|
||||
if (typeof ReactFiberErrorDialogWWW.showErrorDialog !== 'function') {
|
||||
throw new Error(
|
||||
'Expected ReactFiberErrorDialog.showErrorDialog to be a function.',
|
||||
);
|
||||
}
|
||||
|
||||
function wwwOnUncaughtError(
|
||||
error: mixed,
|
||||
errorInfo: {+componentStack?: ?string},
|
||||
): void {
|
||||
const componentStack =
|
||||
errorInfo.componentStack != null ? errorInfo.componentStack : '';
|
||||
const logError = ReactFiberErrorDialogWWW.showErrorDialog({
|
||||
errorBoundary: null,
|
||||
error,
|
||||
componentStack,
|
||||
});
|
||||
|
||||
// Allow injected showErrorDialog() to prevent default console.error logging.
|
||||
// This enables renderers like ReactNative to better manage redbox behavior.
|
||||
if (logError === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
defaultOnUncaughtError(error, errorInfo);
|
||||
}
|
||||
|
||||
function wwwOnCaughtError(
|
||||
error: mixed,
|
||||
errorInfo: {
|
||||
+componentStack?: ?string,
|
||||
+errorBoundary?: ?React$Component<any, any>,
|
||||
},
|
||||
): void {
|
||||
const errorBoundary = errorInfo.errorBoundary;
|
||||
const componentStack =
|
||||
errorInfo.componentStack != null ? errorInfo.componentStack : '';
|
||||
const logError = ReactFiberErrorDialogWWW.showErrorDialog({
|
||||
errorBoundary,
|
||||
error,
|
||||
componentStack,
|
||||
});
|
||||
|
||||
// Allow injected showErrorDialog() to prevent default console.error logging.
|
||||
// This enables renderers like ReactNative to better manage redbox behavior.
|
||||
if (logError === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
defaultOnCaughtError(error, errorInfo);
|
||||
}
|
||||
|
||||
export function createRoot(
|
||||
container: Element | Document | DocumentFragment,
|
||||
options?: CreateRootOptions,
|
||||
): RootType {
|
||||
return createRootImpl(
|
||||
container,
|
||||
assign(
|
||||
({
|
||||
onUncaughtError: wwwOnUncaughtError,
|
||||
onCaughtError: wwwOnCaughtError,
|
||||
}: any),
|
||||
options,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function hydrateRoot(
|
||||
container: Document | Element,
|
||||
initialChildren: ReactNodeList,
|
||||
options?: HydrateRootOptions,
|
||||
): RootType {
|
||||
return hydrateRootImpl(
|
||||
container,
|
||||
initialChildren,
|
||||
assign(
|
||||
({
|
||||
onUncaughtError: wwwOnUncaughtError,
|
||||
onCaughtError: wwwOnCaughtError,
|
||||
}: any),
|
||||
options,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let topLevelUpdateWarnings;
|
||||
|
||||
if (__DEV__) {
|
||||
topLevelUpdateWarnings = (container: Container) => {
|
||||
if (container._reactRootContainer && container.nodeType !== COMMENT_NODE) {
|
||||
const hostInstance = findHostInstanceWithNoPortals(
|
||||
container._reactRootContainer.current,
|
||||
);
|
||||
if (hostInstance) {
|
||||
if (hostInstance.parentNode !== container) {
|
||||
console.error(
|
||||
'It looks like the React-rendered content of this ' +
|
||||
'container was removed without using React. This is not ' +
|
||||
'supported and will cause errors. Instead, call ' +
|
||||
'ReactDOM.unmountComponentAtNode to empty a container.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isRootRenderedBySomeReact = !!container._reactRootContainer;
|
||||
const rootEl = getReactRootElementInContainer(container);
|
||||
const hasNonRootReactChild = !!(rootEl && getInstanceFromNode(rootEl));
|
||||
|
||||
if (hasNonRootReactChild && !isRootRenderedBySomeReact) {
|
||||
console.error(
|
||||
'Replacing React-rendered children with a new root ' +
|
||||
'component. If you intended to update the children of this node, ' +
|
||||
'you should instead have the existing children update their state ' +
|
||||
'and render the new components instead of calling ReactDOM.render.',
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getReactRootElementInContainer(container: any) {
|
||||
if (!container) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (container.nodeType === DOCUMENT_NODE) {
|
||||
return container.documentElement;
|
||||
} else {
|
||||
return container.firstChild;
|
||||
}
|
||||
}
|
||||
|
||||
function noopOnRecoverableError() {
|
||||
// This isn't reachable because onRecoverableError isn't called in the
|
||||
// legacy API.
|
||||
}
|
||||
|
||||
function legacyCreateRootFromDOMContainer(
|
||||
container: Container,
|
||||
initialChildren: ReactNodeList,
|
||||
parentComponent: ?React$Component<any, any>,
|
||||
callback: ?Function,
|
||||
isHydrationContainer: boolean,
|
||||
): FiberRoot {
|
||||
if (isHydrationContainer) {
|
||||
if (typeof callback === 'function') {
|
||||
const originalCallback = callback;
|
||||
callback = function () {
|
||||
const instance = getPublicRootInstance(root);
|
||||
originalCallback.call(instance);
|
||||
};
|
||||
}
|
||||
|
||||
const root: FiberRoot = createHydrationContainer(
|
||||
initialChildren,
|
||||
callback,
|
||||
container,
|
||||
LegacyRoot,
|
||||
null, // hydrationCallbacks
|
||||
false, // isStrictMode
|
||||
false, // concurrentUpdatesByDefaultOverride,
|
||||
'', // identifierPrefix
|
||||
wwwOnUncaughtError,
|
||||
wwwOnCaughtError,
|
||||
noopOnRecoverableError,
|
||||
// TODO(luna) Support hydration later
|
||||
null,
|
||||
null,
|
||||
);
|
||||
container._reactRootContainer = root;
|
||||
markContainerAsRoot(root.current, container);
|
||||
|
||||
const rootContainerElement =
|
||||
container.nodeType === COMMENT_NODE ? container.parentNode : container;
|
||||
// $FlowFixMe[incompatible-call]
|
||||
listenToAllSupportedEvents(rootContainerElement);
|
||||
|
||||
flushSync();
|
||||
return root;
|
||||
} else {
|
||||
// First clear any existing content.
|
||||
clearContainer(container);
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
const originalCallback = callback;
|
||||
callback = function () {
|
||||
const instance = getPublicRootInstance(root);
|
||||
originalCallback.call(instance);
|
||||
};
|
||||
}
|
||||
|
||||
const root = createContainer(
|
||||
container,
|
||||
LegacyRoot,
|
||||
null, // hydrationCallbacks
|
||||
false, // isStrictMode
|
||||
false, // concurrentUpdatesByDefaultOverride,
|
||||
'', // identifierPrefix
|
||||
wwwOnUncaughtError,
|
||||
wwwOnCaughtError,
|
||||
noopOnRecoverableError,
|
||||
null, // transitionCallbacks
|
||||
);
|
||||
container._reactRootContainer = root;
|
||||
markContainerAsRoot(root.current, container);
|
||||
|
||||
const rootContainerElement =
|
||||
container.nodeType === COMMENT_NODE ? container.parentNode : container;
|
||||
// $FlowFixMe[incompatible-call]
|
||||
listenToAllSupportedEvents(rootContainerElement);
|
||||
|
||||
// Initial mount should not be batched.
|
||||
flushSync(() => {
|
||||
updateContainer(initialChildren, root, parentComponent, callback);
|
||||
});
|
||||
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
||||
function warnOnInvalidCallback(callback: mixed): void {
|
||||
if (__DEV__) {
|
||||
if (callback !== null && typeof callback !== 'function') {
|
||||
console.error(
|
||||
'Expected the last optional `callback` argument to be a ' +
|
||||
'function. Instead received: %s.',
|
||||
callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function legacyRenderSubtreeIntoContainer(
|
||||
parentComponent: ?React$Component<any, any>,
|
||||
children: ReactNodeList,
|
||||
container: Container,
|
||||
forceHydrate: boolean,
|
||||
callback: ?Function,
|
||||
): React$Component<any, any> | PublicInstance | null {
|
||||
if (__DEV__) {
|
||||
topLevelUpdateWarnings(container);
|
||||
warnOnInvalidCallback(callback === undefined ? null : callback);
|
||||
}
|
||||
|
||||
const maybeRoot = container._reactRootContainer;
|
||||
let root: FiberRoot;
|
||||
if (!maybeRoot) {
|
||||
// Initial mount
|
||||
root = legacyCreateRootFromDOMContainer(
|
||||
container,
|
||||
children,
|
||||
parentComponent,
|
||||
callback,
|
||||
forceHydrate,
|
||||
);
|
||||
} else {
|
||||
root = maybeRoot;
|
||||
if (typeof callback === 'function') {
|
||||
const originalCallback = callback;
|
||||
callback = function () {
|
||||
const instance = getPublicRootInstance(root);
|
||||
originalCallback.call(instance);
|
||||
};
|
||||
}
|
||||
// Update
|
||||
updateContainer(children, root, parentComponent, callback);
|
||||
}
|
||||
return getPublicRootInstance(root);
|
||||
}
|
||||
|
||||
export function render(
|
||||
element: React$Element<any>,
|
||||
container: Container,
|
||||
callback: ?Function,
|
||||
): React$Component<any, any> | PublicInstance | null {
|
||||
if (disableLegacyMode) {
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
'ReactDOM.render was removed in React 19. Use createRoot instead.',
|
||||
);
|
||||
}
|
||||
throw new Error('ReactDOM: Unsupported Legacy Mode API.');
|
||||
}
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
'ReactDOM.render has not been supported since React 18. Use createRoot ' +
|
||||
'instead. Until you switch to the new API, your app will behave as ' +
|
||||
"if it's running React 17. Learn " +
|
||||
'more: https://react.dev/link/switch-to-createroot',
|
||||
);
|
||||
}
|
||||
|
||||
if (!isValidContainerLegacy(container)) {
|
||||
throw new Error('Target container is not a DOM element.');
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
const isModernRoot =
|
||||
isContainerMarkedAsRoot(container) &&
|
||||
container._reactRootContainer === undefined;
|
||||
if (isModernRoot) {
|
||||
console.error(
|
||||
'You are calling ReactDOM.render() on a container that was previously ' +
|
||||
'passed to ReactDOMClient.createRoot(). This is not supported. ' +
|
||||
'Did you mean to call root.render(element)?',
|
||||
);
|
||||
}
|
||||
}
|
||||
return legacyRenderSubtreeIntoContainer(
|
||||
null,
|
||||
element,
|
||||
container,
|
||||
false,
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
export function unstable_renderSubtreeIntoContainer(
|
||||
parentComponent: React$Component<any, any>,
|
||||
element: React$Element<any>,
|
||||
containerNode: Container,
|
||||
callback: ?Function,
|
||||
): React$Component<any, any> | PublicInstance | null {
|
||||
if (disableLegacyMode) {
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
'ReactDOM.unstable_renderSubtreeIntoContainer() was removed in React 19. Consider using a portal instead.',
|
||||
);
|
||||
}
|
||||
throw new Error('ReactDOM: Unsupported Legacy Mode API.');
|
||||
}
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported ' +
|
||||
'since React 18. Consider using a portal instead. Until you switch to ' +
|
||||
"the createRoot API, your app will behave as if it's running React " +
|
||||
'17. Learn more: https://react.dev/link/switch-to-createroot',
|
||||
);
|
||||
}
|
||||
|
||||
if (!isValidContainerLegacy(containerNode)) {
|
||||
throw new Error('Target container is not a DOM element.');
|
||||
}
|
||||
|
||||
if (parentComponent == null || !hasInstance(parentComponent)) {
|
||||
throw new Error('parentComponent must be a valid React Component');
|
||||
}
|
||||
|
||||
return legacyRenderSubtreeIntoContainer(
|
||||
parentComponent,
|
||||
element,
|
||||
containerNode,
|
||||
false,
|
||||
callback,
|
||||
);
|
||||
}
|
||||
12
packages/react-dom/src/client/__mocks__/ReactFiberErrorDialog.js
vendored
Normal file
12
packages/react-dom/src/client/__mocks__/ReactFiberErrorDialog.js
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export function showErrorDialog(): boolean {
|
||||
return true;
|
||||
}
|
||||
@@ -20,6 +20,9 @@ import {
|
||||
updateContainer,
|
||||
injectIntoDevTools,
|
||||
getPublicRootInstance,
|
||||
defaultOnUncaughtError,
|
||||
defaultOnCaughtError,
|
||||
defaultOnRecoverableError,
|
||||
} from 'react-reconciler/src/ReactFiberReconciler';
|
||||
|
||||
import {createPortal as createPortalImpl} from 'react-reconciler/src/ReactPortal';
|
||||
@@ -43,11 +46,58 @@ import {
|
||||
} from './ReactNativePublicCompat';
|
||||
import {getPublicInstanceFromInternalInstanceHandle} from './ReactFiberConfigFabric';
|
||||
|
||||
// $FlowFixMe[missing-local-annot]
|
||||
function onRecoverableError(error) {
|
||||
// TODO: Expose onRecoverableError option to userspace
|
||||
// eslint-disable-next-line react-internal/no-production-logging, react-internal/warning-args
|
||||
console.error(error);
|
||||
// Module provided by RN:
|
||||
import {ReactFiberErrorDialog} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
|
||||
|
||||
if (typeof ReactFiberErrorDialog.showErrorDialog !== 'function') {
|
||||
throw new Error(
|
||||
'Expected ReactFiberErrorDialog.showErrorDialog to be a function.',
|
||||
);
|
||||
}
|
||||
|
||||
function nativeOnUncaughtError(
|
||||
error: mixed,
|
||||
errorInfo: {+componentStack?: ?string},
|
||||
): void {
|
||||
const componentStack =
|
||||
errorInfo.componentStack != null ? errorInfo.componentStack : '';
|
||||
const logError = ReactFiberErrorDialog.showErrorDialog({
|
||||
errorBoundary: null,
|
||||
error,
|
||||
componentStack,
|
||||
});
|
||||
|
||||
// Allow injected showErrorDialog() to prevent default console.error logging.
|
||||
// This enables renderers like ReactNative to better manage redbox behavior.
|
||||
if (logError === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
defaultOnUncaughtError(error, errorInfo);
|
||||
}
|
||||
function nativeOnCaughtError(
|
||||
error: mixed,
|
||||
errorInfo: {
|
||||
+componentStack?: ?string,
|
||||
+errorBoundary?: ?React$Component<any, any>,
|
||||
},
|
||||
): void {
|
||||
const errorBoundary = errorInfo.errorBoundary;
|
||||
const componentStack =
|
||||
errorInfo.componentStack != null ? errorInfo.componentStack : '';
|
||||
const logError = ReactFiberErrorDialog.showErrorDialog({
|
||||
errorBoundary,
|
||||
error,
|
||||
componentStack,
|
||||
});
|
||||
|
||||
// Allow injected showErrorDialog() to prevent default console.error logging.
|
||||
// This enables renderers like ReactNative to better manage redbox behavior.
|
||||
if (logError === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
defaultOnCaughtError(error, errorInfo);
|
||||
}
|
||||
|
||||
function render(
|
||||
@@ -68,7 +118,9 @@ function render(
|
||||
false,
|
||||
null,
|
||||
'',
|
||||
onRecoverableError,
|
||||
nativeOnUncaughtError,
|
||||
nativeOnCaughtError,
|
||||
defaultOnRecoverableError,
|
||||
null,
|
||||
);
|
||||
roots.set(containerTag, root);
|
||||
|
||||
@@ -20,6 +20,9 @@ import {
|
||||
updateContainer,
|
||||
injectIntoDevTools,
|
||||
getPublicRootInstance,
|
||||
defaultOnUncaughtError,
|
||||
defaultOnCaughtError,
|
||||
defaultOnRecoverableError,
|
||||
} from 'react-reconciler/src/ReactFiberReconciler';
|
||||
// TODO: direct imports like some-package/src/* are bad. Fix me.
|
||||
import {getStackByFiberInDevAndProd} from 'react-reconciler/src/ReactFiberComponentStack';
|
||||
@@ -47,11 +50,58 @@ import {
|
||||
isChildPublicInstance,
|
||||
} from './ReactNativePublicCompat';
|
||||
|
||||
// $FlowFixMe[missing-local-annot]
|
||||
function onRecoverableError(error) {
|
||||
// TODO: Expose onRecoverableError option to userspace
|
||||
// eslint-disable-next-line react-internal/no-production-logging, react-internal/warning-args
|
||||
console.error(error);
|
||||
// Module provided by RN:
|
||||
import {ReactFiberErrorDialog} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
|
||||
|
||||
if (typeof ReactFiberErrorDialog.showErrorDialog !== 'function') {
|
||||
throw new Error(
|
||||
'Expected ReactFiberErrorDialog.showErrorDialog to be a function.',
|
||||
);
|
||||
}
|
||||
|
||||
function nativeOnUncaughtError(
|
||||
error: mixed,
|
||||
errorInfo: {+componentStack?: ?string},
|
||||
): void {
|
||||
const componentStack =
|
||||
errorInfo.componentStack != null ? errorInfo.componentStack : '';
|
||||
const logError = ReactFiberErrorDialog.showErrorDialog({
|
||||
errorBoundary: null,
|
||||
error,
|
||||
componentStack,
|
||||
});
|
||||
|
||||
// Allow injected showErrorDialog() to prevent default console.error logging.
|
||||
// This enables renderers like ReactNative to better manage redbox behavior.
|
||||
if (logError === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
defaultOnUncaughtError(error, errorInfo);
|
||||
}
|
||||
function nativeOnCaughtError(
|
||||
error: mixed,
|
||||
errorInfo: {
|
||||
+componentStack?: ?string,
|
||||
+errorBoundary?: ?React$Component<any, any>,
|
||||
},
|
||||
): void {
|
||||
const errorBoundary = errorInfo.errorBoundary;
|
||||
const componentStack =
|
||||
errorInfo.componentStack != null ? errorInfo.componentStack : '';
|
||||
const logError = ReactFiberErrorDialog.showErrorDialog({
|
||||
errorBoundary,
|
||||
error,
|
||||
componentStack,
|
||||
});
|
||||
|
||||
// Allow injected showErrorDialog() to prevent default console.error logging.
|
||||
// This enables renderers like ReactNative to better manage redbox behavior.
|
||||
if (logError === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
defaultOnCaughtError(error, errorInfo);
|
||||
}
|
||||
|
||||
function render(
|
||||
@@ -71,7 +121,9 @@ function render(
|
||||
false,
|
||||
null,
|
||||
'',
|
||||
onRecoverableError,
|
||||
nativeOnUncaughtError,
|
||||
nativeOnCaughtError,
|
||||
defaultOnRecoverableError,
|
||||
null,
|
||||
);
|
||||
roots.set(containerTag, root);
|
||||
|
||||
@@ -974,6 +974,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
||||
null,
|
||||
false,
|
||||
'',
|
||||
NoopRenderer.defaultOnUncaughtError,
|
||||
NoopRenderer.defaultOnCaughtError,
|
||||
onRecoverableError,
|
||||
null,
|
||||
);
|
||||
@@ -996,6 +998,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
||||
null,
|
||||
false,
|
||||
'',
|
||||
NoopRenderer.defaultOnUncaughtError,
|
||||
NoopRenderer.defaultOnCaughtError,
|
||||
onRecoverableError,
|
||||
options && options.unstable_transitionCallbacks
|
||||
? options.unstable_transitionCallbacks
|
||||
@@ -1028,6 +1032,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
||||
null,
|
||||
false,
|
||||
'',
|
||||
NoopRenderer.defaultOnUncaughtError,
|
||||
NoopRenderer.defaultOnCaughtError,
|
||||
onRecoverableError,
|
||||
null,
|
||||
);
|
||||
|
||||
@@ -266,7 +266,10 @@ import {
|
||||
createCapturedValueAtFiber,
|
||||
type CapturedValue,
|
||||
} from './ReactCapturedValue';
|
||||
import {createClassErrorUpdate} from './ReactFiberThrow';
|
||||
import {
|
||||
createClassErrorUpdate,
|
||||
initializeClassErrorUpdate,
|
||||
} from './ReactFiberThrow';
|
||||
import is from 'shared/objectIs';
|
||||
import {
|
||||
getForksAtLevel,
|
||||
@@ -1179,10 +1182,18 @@ function updateClassComponent(
|
||||
const lane = pickArbitraryLane(renderLanes);
|
||||
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
|
||||
// Schedule the error boundary to re-render using updated state
|
||||
const update = createClassErrorUpdate(
|
||||
const root: FiberRoot | null = getWorkInProgressRoot();
|
||||
if (root === null) {
|
||||
throw new Error(
|
||||
'Expected a work-in-progress root. This is a bug in React. Please file an issue.',
|
||||
);
|
||||
}
|
||||
const update = createClassErrorUpdate(lane);
|
||||
initializeClassErrorUpdate(
|
||||
update,
|
||||
root,
|
||||
workInProgress,
|
||||
createCapturedValueAtFiber(error, workInProgress),
|
||||
lane,
|
||||
);
|
||||
enqueueCapturedUpdate(workInProgress, update);
|
||||
break;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and 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 type {Fiber} from './ReactInternalTypes';
|
||||
import type {CapturedValue} from './ReactCapturedValue';
|
||||
|
||||
// This module is forked in different environments.
|
||||
// By default, return `true` to log errors to the console.
|
||||
// Forks can return `false` if this isn't desirable.
|
||||
|
||||
export function showErrorDialog(
|
||||
boundary: Fiber,
|
||||
errorInfo: CapturedValue<mixed>,
|
||||
): boolean {
|
||||
return true;
|
||||
}
|
||||
@@ -7,99 +7,152 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {Fiber} from './ReactInternalTypes';
|
||||
import type {Fiber, FiberRoot} from './ReactInternalTypes';
|
||||
import type {CapturedValue} from './ReactCapturedValue';
|
||||
|
||||
import {showErrorDialog} from './ReactFiberErrorDialog';
|
||||
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
|
||||
import {HostRoot} from 'react-reconciler/src/ReactWorkTags';
|
||||
|
||||
import {ClassComponent} from './ReactWorkTags';
|
||||
|
||||
import reportGlobalError from 'shared/reportGlobalError';
|
||||
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
const {ReactCurrentActQueue} = ReactSharedInternals;
|
||||
|
||||
export function logCapturedError(
|
||||
boundary: Fiber,
|
||||
// Side-channel since I'm not sure we want to make this part of the public API
|
||||
let componentName: null | string = null;
|
||||
let errorBoundaryName: null | string = null;
|
||||
|
||||
export function defaultOnUncaughtError(
|
||||
error: mixed,
|
||||
errorInfo: {+componentStack?: ?string},
|
||||
): void {
|
||||
// Overriding this can silence these warnings e.g. for tests.
|
||||
// See https://github.com/facebook/react/pull/13384
|
||||
|
||||
// For uncaught root errors we report them as uncaught to the browser's
|
||||
// onerror callback. This won't have component stacks and the error addendum.
|
||||
// So we add those into a separate console.warn.
|
||||
reportGlobalError(error);
|
||||
if (__DEV__) {
|
||||
const componentStack =
|
||||
errorInfo.componentStack != null ? errorInfo.componentStack : '';
|
||||
|
||||
const componentNameMessage = componentName
|
||||
? `An error occurred in the <${componentName}> component:`
|
||||
: 'An error occurred in one of your React components:';
|
||||
|
||||
console['warn'](
|
||||
'%s\n%s\n\n%s',
|
||||
componentNameMessage,
|
||||
componentStack || '',
|
||||
'Consider adding an error boundary to your tree to customize error handling behavior.\n' +
|
||||
'Visit https://react.dev/link/error-boundaries to learn more about error boundaries.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function defaultOnCaughtError(
|
||||
error: mixed,
|
||||
errorInfo: {
|
||||
+componentStack?: ?string,
|
||||
+errorBoundary?: ?React$Component<any, any>,
|
||||
},
|
||||
): void {
|
||||
// Overriding this can silence these warnings e.g. for tests.
|
||||
// See https://github.com/facebook/react/pull/13384
|
||||
|
||||
// Caught by error boundary
|
||||
if (__DEV__) {
|
||||
const componentStack =
|
||||
errorInfo.componentStack != null ? errorInfo.componentStack : '';
|
||||
|
||||
const componentNameMessage = componentName
|
||||
? `The above error occurred in the <${componentName}> component:`
|
||||
: 'The above error occurred in one of your React components:';
|
||||
|
||||
// In development, we provide our own message which includes the component stack
|
||||
// in addition to the error.
|
||||
// Don't transform to our wrapper
|
||||
console['error'](
|
||||
'%o\n\n%s\n%s\n\n%s',
|
||||
error,
|
||||
componentNameMessage,
|
||||
componentStack,
|
||||
`React will try to recreate this component tree from scratch ` +
|
||||
`using the error boundary you provided, ${
|
||||
errorBoundaryName || 'Anonymous'
|
||||
}.`,
|
||||
);
|
||||
} else {
|
||||
// In production, we print the error directly.
|
||||
// This will include the message, the JS stack, and anything the browser wants to show.
|
||||
// We pass the error object instead of custom message so that the browser displays the error natively.
|
||||
console['error'](error); // Don't transform to our wrapper
|
||||
}
|
||||
}
|
||||
|
||||
export function defaultOnRecoverableError(
|
||||
error: mixed,
|
||||
errorInfo: {+digest?: ?string, +componentStack?: ?string},
|
||||
) {
|
||||
reportGlobalError(error);
|
||||
}
|
||||
|
||||
export function logUncaughtError(
|
||||
root: FiberRoot,
|
||||
errorInfo: CapturedValue<mixed>,
|
||||
): void {
|
||||
try {
|
||||
const logError = showErrorDialog(boundary, errorInfo);
|
||||
|
||||
// Allow injected showErrorDialog() to prevent default console.error logging.
|
||||
// This enables renderers like ReactNative to better manage redbox behavior.
|
||||
if (logError === false) {
|
||||
if (__DEV__) {
|
||||
componentName = errorInfo.source
|
||||
? getComponentNameFromFiber(errorInfo.source)
|
||||
: null;
|
||||
errorBoundaryName = null;
|
||||
}
|
||||
const error = (errorInfo.value: any);
|
||||
if (__DEV__ && ReactCurrentActQueue.current !== null) {
|
||||
// For uncaught errors inside act, we track them on the act and then
|
||||
// rethrow them into the test.
|
||||
ReactCurrentActQueue.thrownErrors.push(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const error = (errorInfo.value: any);
|
||||
|
||||
if (boundary.tag === HostRoot) {
|
||||
if (__DEV__ && ReactCurrentActQueue.current !== null) {
|
||||
// For uncaught errors inside act, we track them on the act and then
|
||||
// rethrow them into the test.
|
||||
ReactCurrentActQueue.thrownErrors.push(error);
|
||||
return;
|
||||
}
|
||||
// For uncaught root errors we report them as uncaught to the browser's
|
||||
// onerror callback. This won't have component stacks and the error addendum.
|
||||
// So we add those into a separate console.warn.
|
||||
reportGlobalError(error);
|
||||
if (__DEV__) {
|
||||
const source = errorInfo.source;
|
||||
const stack = errorInfo.stack;
|
||||
const componentStack = stack !== null ? stack : '';
|
||||
// TODO: There's no longer a way to silence these warnings e.g. for tests.
|
||||
// See https://github.com/facebook/react/pull/13384
|
||||
|
||||
const componentName = source ? getComponentNameFromFiber(source) : null;
|
||||
const componentNameMessage = componentName
|
||||
? `An error occurred in the <${componentName}> component:`
|
||||
: 'An error occurred in one of your React components:';
|
||||
|
||||
console['warn'](
|
||||
'%s\n%s\n\n%s',
|
||||
componentNameMessage,
|
||||
componentStack,
|
||||
'Consider adding an error boundary to your tree to customize error handling behavior.\n' +
|
||||
'Visit https://react.dev/link/error-boundaries to learn more about error boundaries.',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Caught by error boundary
|
||||
if (__DEV__) {
|
||||
const source = errorInfo.source;
|
||||
const stack = errorInfo.stack;
|
||||
const componentStack = stack !== null ? stack : '';
|
||||
// TODO: There's no longer a way to silence these warnings e.g. for tests.
|
||||
// See https://github.com/facebook/react/pull/13384
|
||||
|
||||
const componentName = source ? getComponentNameFromFiber(source) : null;
|
||||
const componentNameMessage = componentName
|
||||
? `The above error occurred in the <${componentName}> component:`
|
||||
: 'The above error occurred in one of your React components:';
|
||||
|
||||
const errorBoundaryName =
|
||||
getComponentNameFromFiber(boundary) || 'Anonymous';
|
||||
|
||||
// In development, we provide our own message which includes the component stack
|
||||
// in addition to the error.
|
||||
// Don't transform to our wrapper
|
||||
console['error'](
|
||||
'%o\n\n%s\n%s\n\n%s',
|
||||
error,
|
||||
componentNameMessage,
|
||||
componentStack,
|
||||
`React will try to recreate this component tree from scratch ` +
|
||||
`using the error boundary you provided, ${errorBoundaryName}.`,
|
||||
);
|
||||
} else {
|
||||
// In production, we print the error directly.
|
||||
// This will include the message, the JS stack, and anything the browser wants to show.
|
||||
// We pass the error object instead of custom message so that the browser displays the error natively.
|
||||
console['error'](error); // Don't transform to our wrapper
|
||||
}
|
||||
}
|
||||
const onUncaughtError = root.onUncaughtError;
|
||||
onUncaughtError(error, {
|
||||
componentStack: errorInfo.stack,
|
||||
});
|
||||
} catch (e) {
|
||||
// This method must not throw, or React internal state will get messed up.
|
||||
// If console.error is overridden, or logCapturedError() shows a dialog that throws,
|
||||
// we want to report this error outside of the normal stack as a last resort.
|
||||
// https://github.com/facebook/react/issues/13188
|
||||
setTimeout(() => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function logCaughtError(
|
||||
root: FiberRoot,
|
||||
boundary: Fiber,
|
||||
errorInfo: CapturedValue<mixed>,
|
||||
): void {
|
||||
try {
|
||||
if (__DEV__) {
|
||||
componentName = errorInfo.source
|
||||
? getComponentNameFromFiber(errorInfo.source)
|
||||
: null;
|
||||
errorBoundaryName = getComponentNameFromFiber(boundary);
|
||||
}
|
||||
const error = (errorInfo.value: any);
|
||||
const onCaughtError = root.onCaughtError;
|
||||
onCaughtError(error, {
|
||||
componentStack: errorInfo.stack,
|
||||
errorBoundary:
|
||||
boundary.tag === ClassComponent
|
||||
? boundary.stateNode // This should always be the case as long as we only have class boundaries
|
||||
: null,
|
||||
});
|
||||
} catch (e) {
|
||||
// This method must not throw, or React internal state will get messed up.
|
||||
// If console.error is overridden, or logCapturedError() shows a dialog that throws,
|
||||
|
||||
@@ -111,6 +111,11 @@ export {
|
||||
observeVisibleRects,
|
||||
} from './ReactTestSelectors';
|
||||
export {startHostTransition} from './ReactFiberHooks';
|
||||
export {
|
||||
defaultOnUncaughtError,
|
||||
defaultOnCaughtError,
|
||||
defaultOnRecoverableError,
|
||||
} from './ReactFiberErrorLogger';
|
||||
|
||||
type OpaqueRoot = FiberRoot;
|
||||
|
||||
@@ -249,7 +254,21 @@ export function createContainer(
|
||||
isStrictMode: boolean,
|
||||
concurrentUpdatesByDefaultOverride: null | boolean,
|
||||
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,
|
||||
transitionCallbacks: null | TransitionTracingCallbacks,
|
||||
): OpaqueRoot {
|
||||
const hydrate = false;
|
||||
@@ -263,6 +282,8 @@ export function createContainer(
|
||||
isStrictMode,
|
||||
concurrentUpdatesByDefaultOverride,
|
||||
identifierPrefix,
|
||||
onUncaughtError,
|
||||
onCaughtError,
|
||||
onRecoverableError,
|
||||
transitionCallbacks,
|
||||
null,
|
||||
@@ -279,7 +300,21 @@ export function createHydrationContainer(
|
||||
isStrictMode: boolean,
|
||||
concurrentUpdatesByDefaultOverride: null | boolean,
|
||||
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,
|
||||
transitionCallbacks: null | TransitionTracingCallbacks,
|
||||
formState: ReactFormState<any, any> | null,
|
||||
): OpaqueRoot {
|
||||
@@ -293,6 +328,8 @@ export function createHydrationContainer(
|
||||
isStrictMode,
|
||||
concurrentUpdatesByDefaultOverride,
|
||||
identifierPrefix,
|
||||
onUncaughtError,
|
||||
onCaughtError,
|
||||
onRecoverableError,
|
||||
transitionCallbacks,
|
||||
formState,
|
||||
|
||||
22
packages/react-reconciler/src/ReactFiberRoot.js
vendored
22
packages/react-reconciler/src/ReactFiberRoot.js
vendored
@@ -51,6 +51,8 @@ function FiberRootNode(
|
||||
tag,
|
||||
hydrate: any,
|
||||
identifierPrefix: any,
|
||||
onUncaughtError: any,
|
||||
onCaughtError: any,
|
||||
onRecoverableError: any,
|
||||
formState: ReactFormState<any, any> | null,
|
||||
) {
|
||||
@@ -83,6 +85,8 @@ function FiberRootNode(
|
||||
this.hiddenUpdates = createLaneMap(null);
|
||||
|
||||
this.identifierPrefix = identifierPrefix;
|
||||
this.onUncaughtError = onUncaughtError;
|
||||
this.onCaughtError = onCaughtError;
|
||||
this.onRecoverableError = onRecoverableError;
|
||||
|
||||
if (enableCache) {
|
||||
@@ -143,7 +147,21 @@ export function createFiberRoot(
|
||||
// them through the root constructor. Perhaps we should put them all into a
|
||||
// single type, like a DynamicHostConfig that is defined by the renderer.
|
||||
identifierPrefix: string,
|
||||
onRecoverableError: null | ((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,
|
||||
transitionCallbacks: null | TransitionTracingCallbacks,
|
||||
formState: ReactFormState<any, any> | null,
|
||||
): FiberRoot {
|
||||
@@ -153,6 +171,8 @@ export function createFiberRoot(
|
||||
tag,
|
||||
hydrate,
|
||||
identifierPrefix,
|
||||
onUncaughtError,
|
||||
onCaughtError,
|
||||
onRecoverableError,
|
||||
formState,
|
||||
): any);
|
||||
|
||||
46
packages/react-reconciler/src/ReactFiberThrow.js
vendored
46
packages/react-reconciler/src/ReactFiberThrow.js
vendored
@@ -66,7 +66,7 @@ import {
|
||||
renderDidSuspend,
|
||||
} from './ReactFiberWorkLoop';
|
||||
import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext';
|
||||
import {logCapturedError} from './ReactFiberErrorLogger';
|
||||
import {logUncaughtError, logCaughtError} from './ReactFiberErrorLogger';
|
||||
import {logComponentSuspended} from './DebugTracing';
|
||||
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
|
||||
import {
|
||||
@@ -85,7 +85,7 @@ import {noopSuspenseyCommitThenable} from './ReactFiberThenable';
|
||||
import {REACT_POSTPONE_TYPE} from 'shared/ReactSymbols';
|
||||
|
||||
function createRootErrorUpdate(
|
||||
fiber: Fiber,
|
||||
root: FiberRoot,
|
||||
errorInfo: CapturedValue<mixed>,
|
||||
lane: Lane,
|
||||
): Update<mixed> {
|
||||
@@ -96,18 +96,23 @@ function createRootErrorUpdate(
|
||||
// being called "element".
|
||||
update.payload = {element: null};
|
||||
update.callback = () => {
|
||||
logCapturedError(fiber, errorInfo);
|
||||
logUncaughtError(root, errorInfo);
|
||||
};
|
||||
return update;
|
||||
}
|
||||
|
||||
function createClassErrorUpdate(
|
||||
fiber: Fiber,
|
||||
errorInfo: CapturedValue<mixed>,
|
||||
lane: Lane,
|
||||
): Update<mixed> {
|
||||
function createClassErrorUpdate(lane: Lane): Update<mixed> {
|
||||
const update = createUpdate(lane);
|
||||
update.tag = CaptureUpdate;
|
||||
return update;
|
||||
}
|
||||
|
||||
function initializeClassErrorUpdate(
|
||||
update: Update<mixed>,
|
||||
root: FiberRoot,
|
||||
fiber: Fiber,
|
||||
errorInfo: CapturedValue<mixed>,
|
||||
): void {
|
||||
const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
|
||||
if (typeof getDerivedStateFromError === 'function') {
|
||||
const error = errorInfo.value;
|
||||
@@ -118,7 +123,7 @@ function createClassErrorUpdate(
|
||||
if (__DEV__) {
|
||||
markFailedErrorBoundaryForHotReloading(fiber);
|
||||
}
|
||||
logCapturedError(fiber, errorInfo);
|
||||
logCaughtError(root, fiber, errorInfo);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -129,7 +134,7 @@ function createClassErrorUpdate(
|
||||
if (__DEV__) {
|
||||
markFailedErrorBoundaryForHotReloading(fiber);
|
||||
}
|
||||
logCapturedError(fiber, errorInfo);
|
||||
logCaughtError(root, fiber, errorInfo);
|
||||
if (typeof getDerivedStateFromError !== 'function') {
|
||||
// To preserve the preexisting retry behavior of error boundaries,
|
||||
// we keep track of which ones already failed during this batch.
|
||||
@@ -159,7 +164,6 @@ function createClassErrorUpdate(
|
||||
}
|
||||
};
|
||||
}
|
||||
return update;
|
||||
}
|
||||
|
||||
function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
|
||||
@@ -561,7 +565,11 @@ function throwException(
|
||||
workInProgress.flags |= ShouldCapture;
|
||||
const lane = pickArbitraryLane(rootRenderLanes);
|
||||
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
|
||||
const update = createRootErrorUpdate(workInProgress, errorInfo, lane);
|
||||
const update = createRootErrorUpdate(
|
||||
workInProgress.stateNode,
|
||||
errorInfo,
|
||||
lane,
|
||||
);
|
||||
enqueueCapturedUpdate(workInProgress, update);
|
||||
return false;
|
||||
}
|
||||
@@ -581,11 +589,8 @@ function throwException(
|
||||
const lane = pickArbitraryLane(rootRenderLanes);
|
||||
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
|
||||
// Schedule the error boundary to re-render using updated state
|
||||
const update = createClassErrorUpdate(
|
||||
workInProgress,
|
||||
errorInfo,
|
||||
lane,
|
||||
);
|
||||
const update = createClassErrorUpdate(lane);
|
||||
initializeClassErrorUpdate(update, root, workInProgress, errorInfo);
|
||||
enqueueCapturedUpdate(workInProgress, update);
|
||||
return false;
|
||||
}
|
||||
@@ -600,4 +605,9 @@ function throwException(
|
||||
return false;
|
||||
}
|
||||
|
||||
export {throwException, createRootErrorUpdate, createClassErrorUpdate};
|
||||
export {
|
||||
throwException,
|
||||
createRootErrorUpdate,
|
||||
createClassErrorUpdate,
|
||||
initializeClassErrorUpdate,
|
||||
};
|
||||
|
||||
@@ -177,6 +177,7 @@ import {
|
||||
throwException,
|
||||
createRootErrorUpdate,
|
||||
createClassErrorUpdate,
|
||||
initializeClassErrorUpdate,
|
||||
} from './ReactFiberThrow';
|
||||
import {
|
||||
commitBeforeMutationEffects,
|
||||
@@ -277,7 +278,7 @@ import {
|
||||
} from './ReactFiberRootScheduler';
|
||||
import {getMaskedContext, getUnmaskedContext} from './ReactFiberContext';
|
||||
import {peekEntangledActionLane} from './ReactFiberAsyncAction';
|
||||
import {logCapturedError} from './ReactFiberErrorLogger';
|
||||
import {logUncaughtError} from './ReactFiberErrorLogger';
|
||||
|
||||
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
|
||||
|
||||
@@ -1731,8 +1732,8 @@ function handleThrow(root: FiberRoot, thrownValue: any): void {
|
||||
if (erroredWork === null) {
|
||||
// This is a fatal error
|
||||
workInProgressRootExitStatus = RootFatalErrored;
|
||||
logCapturedError(
|
||||
root.current,
|
||||
logUncaughtError(
|
||||
root,
|
||||
createCapturedValueAtFiber(thrownValue, root.current),
|
||||
);
|
||||
return;
|
||||
@@ -2552,10 +2553,7 @@ function panicOnRootError(root: FiberRoot, error: mixed) {
|
||||
// caught by an error boundary. This is a fatal error, or panic condition,
|
||||
// because we've run out of ways to recover.
|
||||
workInProgressRootExitStatus = RootFatalErrored;
|
||||
logCapturedError(
|
||||
root.current,
|
||||
createCapturedValueAtFiber(error, root.current),
|
||||
);
|
||||
logUncaughtError(root, createCapturedValueAtFiber(error, root.current));
|
||||
// Set `workInProgress` to null. This represents advancing to the next
|
||||
// sibling, or the parent if there are no siblings. But since the root
|
||||
// has no siblings nor a parent, we set it to null. Usually this is
|
||||
@@ -3356,7 +3354,11 @@ function captureCommitPhaseErrorOnRoot(
|
||||
error: mixed,
|
||||
) {
|
||||
const errorInfo = createCapturedValueAtFiber(error, sourceFiber);
|
||||
const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane));
|
||||
const update = createRootErrorUpdate(
|
||||
rootFiber.stateNode,
|
||||
errorInfo,
|
||||
(SyncLane: Lane),
|
||||
);
|
||||
const root = enqueueUpdate(rootFiber, update, (SyncLane: Lane));
|
||||
if (root !== null) {
|
||||
markRootUpdated(root, SyncLane);
|
||||
@@ -3393,13 +3395,10 @@ export function captureCommitPhaseError(
|
||||
!isAlreadyFailedLegacyErrorBoundary(instance))
|
||||
) {
|
||||
const errorInfo = createCapturedValueAtFiber(error, sourceFiber);
|
||||
const update = createClassErrorUpdate(
|
||||
fiber,
|
||||
errorInfo,
|
||||
(SyncLane: Lane),
|
||||
);
|
||||
const update = createClassErrorUpdate((SyncLane: Lane));
|
||||
const root = enqueueUpdate(fiber, update, (SyncLane: Lane));
|
||||
if (root !== null) {
|
||||
initializeClassErrorUpdate(update, root, fiber, errorInfo);
|
||||
markRootUpdated(root, SyncLane);
|
||||
ensureRootIsScheduled(root);
|
||||
}
|
||||
|
||||
@@ -260,9 +260,20 @@ type BaseFiberRootProperties = {
|
||||
// a reference to.
|
||||
identifierPrefix: string,
|
||||
|
||||
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},
|
||||
errorInfo: {+digest?: ?string, +componentStack?: ?string},
|
||||
) => void,
|
||||
|
||||
formState: ReactFormState<any, any> | null,
|
||||
|
||||
229
packages/react-reconciler/src/__tests__/ReactConfigurableErrorLogging-test.js
vendored
Normal file
229
packages/react-reconciler/src/__tests__/ReactConfigurableErrorLogging-test.js
vendored
Normal file
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactDOMClient;
|
||||
let Scheduler;
|
||||
let container;
|
||||
let act;
|
||||
|
||||
async function fakeAct(cb) {
|
||||
// We don't use act/waitForThrow here because we want to observe how errors are reported for real.
|
||||
await cb();
|
||||
Scheduler.unstable_flushAll();
|
||||
}
|
||||
|
||||
describe('ReactConfigurableErrorLogging', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
Scheduler = require('scheduler');
|
||||
container = document.createElement('div');
|
||||
if (__DEV__) {
|
||||
act = React.act;
|
||||
}
|
||||
});
|
||||
|
||||
it('should log errors that occur during the begin phase', async () => {
|
||||
class ErrorThrowingComponent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
throw new Error('constructor error');
|
||||
}
|
||||
render() {
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
const uncaughtErrors = [];
|
||||
const caughtErrors = [];
|
||||
const root = ReactDOMClient.createRoot(container, {
|
||||
onUncaughtError(error, errorInfo) {
|
||||
uncaughtErrors.push(error, errorInfo);
|
||||
},
|
||||
onCaughtError(error, errorInfo) {
|
||||
caughtErrors.push(error, errorInfo);
|
||||
},
|
||||
});
|
||||
await fakeAct(() => {
|
||||
root.render(
|
||||
<div>
|
||||
<span>
|
||||
<ErrorThrowingComponent />
|
||||
</span>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(uncaughtErrors).toEqual([
|
||||
expect.objectContaining({
|
||||
message: 'constructor error',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
componentStack: expect.stringMatching(
|
||||
new RegExp(
|
||||
'\\s+(in|at) ErrorThrowingComponent (.*)\n' +
|
||||
'\\s+(in|at) span(.*)\n' +
|
||||
'\\s+(in|at) div(.*)',
|
||||
),
|
||||
),
|
||||
}),
|
||||
]);
|
||||
expect(caughtErrors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should log errors that occur during the commit phase', async () => {
|
||||
class ErrorThrowingComponent extends React.Component {
|
||||
componentDidMount() {
|
||||
throw new Error('componentDidMount error');
|
||||
}
|
||||
render() {
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
const uncaughtErrors = [];
|
||||
const caughtErrors = [];
|
||||
const root = ReactDOMClient.createRoot(container, {
|
||||
onUncaughtError(error, errorInfo) {
|
||||
uncaughtErrors.push(error, errorInfo);
|
||||
},
|
||||
onCaughtError(error, errorInfo) {
|
||||
caughtErrors.push(error, errorInfo);
|
||||
},
|
||||
});
|
||||
await fakeAct(() => {
|
||||
root.render(
|
||||
<div>
|
||||
<span>
|
||||
<ErrorThrowingComponent />
|
||||
</span>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(uncaughtErrors).toEqual([
|
||||
expect.objectContaining({
|
||||
message: 'componentDidMount error',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
componentStack: expect.stringMatching(
|
||||
new RegExp(
|
||||
'\\s+(in|at) ErrorThrowingComponent (.*)\n' +
|
||||
'\\s+(in|at) span(.*)\n' +
|
||||
'\\s+(in|at) div(.*)',
|
||||
),
|
||||
),
|
||||
}),
|
||||
]);
|
||||
expect(caughtErrors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should ignore errors thrown in log method to prevent cycle', async () => {
|
||||
class ErrorBoundary extends React.Component {
|
||||
state = {error: null};
|
||||
componentDidCatch(error) {
|
||||
this.setState({error});
|
||||
}
|
||||
render() {
|
||||
return this.state.error ? null : this.props.children;
|
||||
}
|
||||
}
|
||||
class ErrorThrowingComponent extends React.Component {
|
||||
render() {
|
||||
throw new Error('render error');
|
||||
}
|
||||
}
|
||||
|
||||
const uncaughtErrors = [];
|
||||
const caughtErrors = [];
|
||||
const root = ReactDOMClient.createRoot(container, {
|
||||
onUncaughtError(error, errorInfo) {
|
||||
uncaughtErrors.push(error, errorInfo);
|
||||
},
|
||||
onCaughtError(error, errorInfo) {
|
||||
caughtErrors.push(error, errorInfo);
|
||||
throw new Error('onCaughtError error');
|
||||
},
|
||||
});
|
||||
|
||||
const ref = React.createRef();
|
||||
|
||||
await fakeAct(() => {
|
||||
root.render(
|
||||
<div>
|
||||
<ErrorBoundary ref={ref}>
|
||||
<span>
|
||||
<ErrorThrowingComponent />
|
||||
</span>
|
||||
</ErrorBoundary>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(uncaughtErrors).toEqual([]);
|
||||
expect(caughtErrors).toEqual([
|
||||
expect.objectContaining({
|
||||
message: 'render error',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
componentStack: expect.stringMatching(
|
||||
new RegExp(
|
||||
'\\s+(in|at) ErrorThrowingComponent (.*)\n' +
|
||||
'\\s+(in|at) span(.*)\n' +
|
||||
'\\s+(in|at) ErrorBoundary(.*)\n' +
|
||||
'\\s+(in|at) div(.*)',
|
||||
),
|
||||
),
|
||||
errorBoundary: ref.current,
|
||||
}),
|
||||
]);
|
||||
|
||||
// The error thrown in caughtError should be rethrown with a clean stack
|
||||
expect(() => {
|
||||
jest.runAllTimers();
|
||||
}).toThrow('onCaughtError error');
|
||||
});
|
||||
|
||||
it('does not log errors when inside real act', async () => {
|
||||
function ErrorThrowingComponent() {
|
||||
throw new Error('render error');
|
||||
}
|
||||
const uncaughtErrors = [];
|
||||
const caughtErrors = [];
|
||||
const root = ReactDOMClient.createRoot(container, {
|
||||
onUncaughtError(error, errorInfo) {
|
||||
uncaughtErrors.push(error, errorInfo);
|
||||
},
|
||||
onCaughtError(error, errorInfo) {
|
||||
caughtErrors.push(error, errorInfo);
|
||||
},
|
||||
});
|
||||
|
||||
if (__DEV__) {
|
||||
global.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
await expect(async () => {
|
||||
await act(() => {
|
||||
root.render(
|
||||
<div>
|
||||
<span>
|
||||
<ErrorThrowingComponent />
|
||||
</span>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
}).rejects.toThrow('render error');
|
||||
}
|
||||
|
||||
expect(uncaughtErrors).toEqual([]);
|
||||
expect(caughtErrors).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -93,7 +93,11 @@ describe('ReactFiberHostContext', () => {
|
||||
ConcurrentRoot,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
'',
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
null,
|
||||
);
|
||||
act(() => {
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and 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 type {Fiber} from '../ReactFiber';
|
||||
import type {CapturedValue} from '../ReactCapturedValue';
|
||||
|
||||
import {ClassComponent} from '../ReactWorkTags';
|
||||
|
||||
// Module provided by RN:
|
||||
import {ReactFiberErrorDialog as RNImpl} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
|
||||
|
||||
if (typeof RNImpl.showErrorDialog !== 'function') {
|
||||
throw new Error(
|
||||
'Expected ReactFiberErrorDialog.showErrorDialog to be a function.',
|
||||
);
|
||||
}
|
||||
|
||||
export function showErrorDialog(
|
||||
boundary: Fiber,
|
||||
errorInfo: CapturedValue<mixed>,
|
||||
): boolean {
|
||||
const capturedError = {
|
||||
componentStack: errorInfo.stack !== null ? errorInfo.stack : '',
|
||||
error: errorInfo.value,
|
||||
errorBoundary:
|
||||
boundary !== null && boundary.tag === ClassComponent
|
||||
? boundary.stateNode
|
||||
: null,
|
||||
};
|
||||
return RNImpl.showErrorDialog(capturedError);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and 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 type {Fiber} from '../ReactFiber';
|
||||
import type {CapturedValue} from '../ReactCapturedValue';
|
||||
|
||||
import {ClassComponent} from '../ReactWorkTags';
|
||||
|
||||
// Provided by www
|
||||
const ReactFiberErrorDialogWWW = require('ReactFiberErrorDialog');
|
||||
|
||||
if (typeof ReactFiberErrorDialogWWW.showErrorDialog !== 'function') {
|
||||
throw new Error(
|
||||
'Expected ReactFiberErrorDialog.showErrorDialog to be a function.',
|
||||
);
|
||||
}
|
||||
|
||||
export function showErrorDialog(
|
||||
boundary: Fiber,
|
||||
errorInfo: CapturedValue<mixed>,
|
||||
): boolean {
|
||||
const capturedError = {
|
||||
componentStack: errorInfo.stack !== null ? errorInfo.stack : '',
|
||||
error: errorInfo.value,
|
||||
errorBoundary:
|
||||
boundary !== null && boundary.tag === ClassComponent
|
||||
? boundary.stateNode
|
||||
: null,
|
||||
};
|
||||
return ReactFiberErrorDialogWWW.showErrorDialog(capturedError);
|
||||
}
|
||||
@@ -23,6 +23,9 @@ import {
|
||||
flushSync,
|
||||
injectIntoDevTools,
|
||||
batchedUpdates,
|
||||
defaultOnUncaughtError,
|
||||
defaultOnCaughtError,
|
||||
defaultOnRecoverableError,
|
||||
} from 'react-reconciler/src/ReactFiberReconciler';
|
||||
import {findCurrentFiberUsingSlowPath} from 'react-reconciler/src/ReactFiberTreeReflection';
|
||||
import {
|
||||
@@ -454,13 +457,6 @@ function propsMatch(props: Object, filter: Object): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// $FlowFixMe[missing-local-annot]
|
||||
function onRecoverableError(error) {
|
||||
// TODO: Expose onRecoverableError option to userspace
|
||||
// eslint-disable-next-line react-internal/no-production-logging, react-internal/warning-args
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
function create(
|
||||
element: React$Element<any>,
|
||||
options: TestRendererOptions,
|
||||
@@ -522,7 +518,9 @@ function create(
|
||||
isStrictMode,
|
||||
concurrentUpdatesByDefault,
|
||||
'',
|
||||
onRecoverableError,
|
||||
defaultOnUncaughtError,
|
||||
defaultOnCaughtError,
|
||||
defaultOnRecoverableError,
|
||||
null,
|
||||
);
|
||||
|
||||
|
||||
@@ -98,6 +98,8 @@ describe('ReactTestRenderer', () => {
|
||||
null,
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -232,36 +232,6 @@ const forks = Object.freeze({
|
||||
}
|
||||
},
|
||||
|
||||
// Different dialogs for caught errors.
|
||||
'./packages/react-reconciler/src/ReactFiberErrorDialog.js': (
|
||||
bundleType,
|
||||
entry
|
||||
) => {
|
||||
switch (bundleType) {
|
||||
case FB_WWW_DEV:
|
||||
case FB_WWW_PROD:
|
||||
case FB_WWW_PROFILING:
|
||||
// Use the www fork which shows an error dialog.
|
||||
return './packages/react-reconciler/src/forks/ReactFiberErrorDialog.www.js';
|
||||
case RN_OSS_DEV:
|
||||
case RN_OSS_PROD:
|
||||
case RN_OSS_PROFILING:
|
||||
case RN_FB_DEV:
|
||||
case RN_FB_PROD:
|
||||
case RN_FB_PROFILING:
|
||||
switch (entry) {
|
||||
case 'react-native-renderer':
|
||||
case 'react-native-renderer/fabric':
|
||||
// Use the RN fork which plays well with redbox.
|
||||
return './packages/react-reconciler/src/forks/ReactFiberErrorDialog.native.js';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
'./packages/react-reconciler/src/ReactFiberConfig.js': (
|
||||
bundleType,
|
||||
entry,
|
||||
|
||||
Reference in New Issue
Block a user