Files
react/packages/react-native-renderer/src/ReactNativeRenderer.js
T
Sebastian Markbåge a053716077 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.
2024-03-27 00:51:37 -04:00

211 lines
5.9 KiB
JavaScript

/**
* 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 {ReactPortal, ReactNodeList} from 'shared/ReactTypes';
import type {ElementRef, Element, ElementType} from 'react';
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
import './ReactNativeInjection';
import {
batchedUpdates as batchedUpdatesImpl,
discreteUpdates,
createContainer,
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';
import {createPortal as createPortalImpl} from 'react-reconciler/src/ReactPortal';
import {
setBatchingImplementation,
batchedUpdates,
} from './legacy-events/ReactGenericBatching';
import ReactVersion from 'shared/ReactVersion';
// Modules provided by RN:
import {UIManager} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
import {getClosestInstanceFromNode} from './ReactNativeComponentTree';
import {
getInspectorDataForViewTag,
getInspectorDataForViewAtPoint,
getInspectorDataForInstance,
} from './ReactNativeFiberInspector';
import {LegacyRoot} from 'react-reconciler/src/ReactRootTags';
import {
findHostInstance_DEPRECATED,
findNodeHandle,
dispatchCommand,
sendAccessibilityEvent,
isChildPublicInstance,
} from './ReactNativePublicCompat';
// 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(
element: Element<ElementType>,
containerTag: number,
callback: ?() => void,
): ?ElementRef<ElementType> {
let root = roots.get(containerTag);
if (!root) {
// TODO (bvaughn): If we decide to keep the wrapper component,
// We could create a wrapper for containerTag as well to reduce special casing.
root = createContainer(
containerTag,
LegacyRoot,
null,
false,
null,
'',
nativeOnUncaughtError,
nativeOnCaughtError,
defaultOnRecoverableError,
null,
);
roots.set(containerTag, root);
}
updateContainer(element, root, null, callback);
return getPublicRootInstance(root);
}
function unmountComponentAtNode(containerTag: number) {
const root = roots.get(containerTag);
if (root) {
// TODO: Is it safe to reset this now or should I wait since this unmount could be deferred?
updateContainer(null, root, null, () => {
roots.delete(containerTag);
});
}
}
function unmountComponentAtNodeAndRemoveContainer(containerTag: number) {
unmountComponentAtNode(containerTag);
// Call back into native to remove all of the subviews from this container
UIManager.removeRootView(containerTag);
}
function createPortal(
children: ReactNodeList,
containerTag: number,
key: ?string = null,
): ReactPortal {
return createPortalImpl(children, containerTag, null, key);
}
setBatchingImplementation(batchedUpdatesImpl, discreteUpdates);
function computeComponentStackForErrorReporting(reactTag: number): string {
const fiber = getClosestInstanceFromNode(reactTag);
if (!fiber) {
return '';
}
return getStackByFiberInDevAndProd(fiber);
}
const roots = new Map<number, FiberRoot>();
const Internals = {
computeComponentStackForErrorReporting,
};
export {
// This is needed for implementation details of TouchableNativeFeedback
// Remove this once TouchableNativeFeedback doesn't use cloneElement
findHostInstance_DEPRECATED,
findNodeHandle,
dispatchCommand,
sendAccessibilityEvent,
render,
unmountComponentAtNode,
unmountComponentAtNodeAndRemoveContainer,
createPortal,
batchedUpdates as unstable_batchedUpdates,
Internals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
// This export is typically undefined in production builds.
// See the "enableGetInspectorDataForInstanceInProduction" flag.
getInspectorDataForInstance,
// DEV-only:
isChildPublicInstance,
};
injectIntoDevTools({
findFiberByHostInstance: getClosestInstanceFromNode,
bundleType: __DEV__ ? 1 : 0,
version: ReactVersion,
rendererPackageName: 'react-native-renderer',
rendererConfig: {
getInspectorDataForInstance,
getInspectorDataForViewTag: getInspectorDataForViewTag,
getInspectorDataForViewAtPoint: getInspectorDataForViewAtPoint.bind(
null,
findNodeHandle,
),
},
});