mirror of
https://github.com/facebook/react.git
synced 2026-02-27 03:07:57 +00:00
* [RFC] Add onHydrationError option to hydrateRoot This is not the final API but I'm pushing it for discussion purposes. When an error is thrown during hydration, we fallback to client rendering, without triggering an error boundary. This is good because, in many cases, the UI will recover and the user won't even notice that something has gone wrong behind the scenes. However, we shouldn't recover from these errors silently, because the underlying cause might be pretty serious. Server-client mismatches are not supposed to happen, even if UI doesn't break from the users perspective. Ignoring them could lead to worse problems later. De-opting from server to client rendering could also be a significant performance regression, depending on the scope of the UI it affects. So we need a way to log when hydration errors occur. This adds a new option for `hydrateRoot` called `onHydrationError`. It's symmetrical to the server renderer's `onError` option, and serves the same purpose. When no option is provided, the default behavior is to schedule a browser task and rethrow the error. This will trigger the normal browser behavior for errors, including dispatching an error event. If the app already has error monitoring, this likely will just work as expected without additional configuration. However, we can also expose additional metadata about these errors, like which Suspense boundaries were affected by the de-opt to client rendering. (I have not exposed any metadata in this commit; API needs more design work.) There are other situations besides hydration where we recover from an error without surfacing it to the user, or notifying an error boundary. For example, if an error occurs during a concurrent render, it could be due to a data race, so we try again synchronously in case that fixes it. We should probably expose a way to log these types of errors, too. (Also not implemented in this commit.) * Log all recoverable errors This expands the scope of onHydrationError to include all errors that are not surfaced to the UI (an error boundary). In addition to errors that occur during hydration, this also includes errors that recoverable by de-opting to synchronous rendering. Typically (or really, by definition) these errors are the result of a concurrent data race; blocking the main thread fixes them by prevents subsequent races. The logic for de-opting to synchronous rendering already existed. The only thing that has changed is that we now log the errors instead of silently proceeding. The logging API has been renamed from onHydrationError to onRecoverableError. * Don't log recoverable errors until commit phase If the render is interrupted and restarts, we don't want to log the errors multiple times. This change only affects errors that are recovered by de-opting to synchronous rendering; we'll have to do something else for errors during hydration, since they use a different recovery path. * Only log hydration error if client render succeeds Similar to previous step. When an error occurs during hydration, we only want to log it if falling back to client rendering _succeeds_. If client rendering fails, the error will get reported to the nearest error boundary, so there's no need for a duplicate log. To implement this, I added a list of errors to the hydration context. If the Suspense boundary successfully completes, they are added to the main recoverable errors queue (the one I added in the previous step.) * Log error with queueMicrotask instead of Scheduler If onRecoverableError is not provided, we default to rethrowing the error in a separate task. Originally, I scheduled the task with idle priority, but @sebmarkbage made the good point that if there are multiple errors logs, we want to preserve the original order. So I've switched it to a microtask. The priority can be lowered in userspace by scheduling an additional task inside onRecoverableError. * Only use host config method for default behavior Redefines the contract of the host config's logRecoverableError method to be a default implementation for onRecoverableError if a user-provided one is not provided when the root is created. * Log with reportError instead of rethrowing In modern browsers, reportError will dispatch an error event, emulating an uncaught JavaScript error. We can do this instead of rethrowing recoverable errors in a microtask, which is nice because it avoids any subtle ordering issues. In older browsers and test environments, we'll fall back to console.error. * Naming nits queueRecoverableHydrationErrors -> upgradeHydrationErrorsToRecoverable
295 lines
8.7 KiB
JavaScript
295 lines
8.7 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its 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 {HostComponent} from './ReactNativeTypes';
|
|
import type {ReactNodeList} from 'shared/ReactTypes';
|
|
import type {ElementRef, Element, ElementType} from 'react';
|
|
|
|
import './ReactNativeInjection';
|
|
|
|
import {
|
|
findHostInstance,
|
|
findHostInstanceWithWarning,
|
|
batchedUpdates as batchedUpdatesImpl,
|
|
discreteUpdates,
|
|
createContainer,
|
|
updateContainer,
|
|
injectIntoDevTools,
|
|
getPublicRootInstance,
|
|
} 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,
|
|
legacySendAccessibilityEvent,
|
|
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
|
|
import {getInspectorDataForInstance} from './ReactNativeFiberInspector';
|
|
|
|
import {getClosestInstanceFromNode} from './ReactNativeComponentTree';
|
|
import {
|
|
getInspectorDataForViewTag,
|
|
getInspectorDataForViewAtPoint,
|
|
} from './ReactNativeFiberInspector';
|
|
import {LegacyRoot} from 'react-reconciler/src/ReactRootTags';
|
|
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
|
import getComponentNameFromType from 'shared/getComponentNameFromType';
|
|
|
|
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
|
|
|
|
function findHostInstance_DEPRECATED(
|
|
componentOrHandle: any,
|
|
): ?React$ElementRef<HostComponent<mixed>> {
|
|
if (__DEV__) {
|
|
const owner = ReactCurrentOwner.current;
|
|
if (owner !== null && owner.stateNode !== null) {
|
|
if (!owner.stateNode._warnedAboutRefsInRender) {
|
|
console.error(
|
|
'%s is accessing findNodeHandle inside its render(). ' +
|
|
'render() should be a pure function of props and state. It should ' +
|
|
'never access something that requires stale data from the previous ' +
|
|
'render, such as refs. Move this logic to componentDidMount and ' +
|
|
'componentDidUpdate instead.',
|
|
getComponentNameFromType(owner.type) || 'A component',
|
|
);
|
|
}
|
|
|
|
owner.stateNode._warnedAboutRefsInRender = true;
|
|
}
|
|
}
|
|
if (componentOrHandle == null) {
|
|
return null;
|
|
}
|
|
if (componentOrHandle._nativeTag) {
|
|
return componentOrHandle;
|
|
}
|
|
if (componentOrHandle.canonical && componentOrHandle.canonical._nativeTag) {
|
|
return componentOrHandle.canonical;
|
|
}
|
|
let hostInstance;
|
|
if (__DEV__) {
|
|
hostInstance = findHostInstanceWithWarning(
|
|
componentOrHandle,
|
|
'findHostInstance_DEPRECATED',
|
|
);
|
|
} else {
|
|
hostInstance = findHostInstance(componentOrHandle);
|
|
}
|
|
|
|
if (hostInstance == null) {
|
|
return hostInstance;
|
|
}
|
|
if ((hostInstance: any).canonical) {
|
|
// Fabric
|
|
return (hostInstance: any).canonical;
|
|
}
|
|
// $FlowFixMe[incompatible-return]
|
|
return hostInstance;
|
|
}
|
|
|
|
function findNodeHandle(componentOrHandle: any): ?number {
|
|
if (__DEV__) {
|
|
const owner = ReactCurrentOwner.current;
|
|
if (owner !== null && owner.stateNode !== null) {
|
|
if (!owner.stateNode._warnedAboutRefsInRender) {
|
|
console.error(
|
|
'%s is accessing findNodeHandle inside its render(). ' +
|
|
'render() should be a pure function of props and state. It should ' +
|
|
'never access something that requires stale data from the previous ' +
|
|
'render, such as refs. Move this logic to componentDidMount and ' +
|
|
'componentDidUpdate instead.',
|
|
getComponentNameFromType(owner.type) || 'A component',
|
|
);
|
|
}
|
|
|
|
owner.stateNode._warnedAboutRefsInRender = true;
|
|
}
|
|
}
|
|
if (componentOrHandle == null) {
|
|
return null;
|
|
}
|
|
if (typeof componentOrHandle === 'number') {
|
|
// Already a node handle
|
|
return componentOrHandle;
|
|
}
|
|
if (componentOrHandle._nativeTag) {
|
|
return componentOrHandle._nativeTag;
|
|
}
|
|
if (componentOrHandle.canonical && componentOrHandle.canonical._nativeTag) {
|
|
return componentOrHandle.canonical._nativeTag;
|
|
}
|
|
let hostInstance;
|
|
if (__DEV__) {
|
|
hostInstance = findHostInstanceWithWarning(
|
|
componentOrHandle,
|
|
'findNodeHandle',
|
|
);
|
|
} else {
|
|
hostInstance = findHostInstance(componentOrHandle);
|
|
}
|
|
|
|
if (hostInstance == null) {
|
|
return hostInstance;
|
|
}
|
|
if ((hostInstance: any).canonical) {
|
|
// Fabric
|
|
return (hostInstance: any).canonical._nativeTag;
|
|
}
|
|
return hostInstance._nativeTag;
|
|
}
|
|
|
|
function dispatchCommand(handle: any, command: string, args: Array<any>) {
|
|
if (handle._nativeTag == null) {
|
|
if (__DEV__) {
|
|
console.error(
|
|
"dispatchCommand was called with a ref that isn't a " +
|
|
'native component. Use React.forwardRef to get access to the underlying native component',
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (handle._internalInstanceHandle != null) {
|
|
const {stateNode} = handle._internalInstanceHandle;
|
|
if (stateNode != null) {
|
|
nativeFabricUIManager.dispatchCommand(stateNode.node, command, args);
|
|
}
|
|
} else {
|
|
UIManager.dispatchViewManagerCommand(handle._nativeTag, command, args);
|
|
}
|
|
}
|
|
|
|
function sendAccessibilityEvent(handle: any, eventType: string) {
|
|
if (handle._nativeTag == null) {
|
|
if (__DEV__) {
|
|
console.error(
|
|
"sendAccessibilityEvent was called with a ref that isn't a " +
|
|
'native component. Use React.forwardRef to get access to the underlying native component',
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (handle._internalInstanceHandle != null) {
|
|
const {stateNode} = handle._internalInstanceHandle;
|
|
if (stateNode != null) {
|
|
nativeFabricUIManager.sendAccessibilityEvent(stateNode.node, eventType);
|
|
}
|
|
} else {
|
|
legacySendAccessibilityEvent(handle._nativeTag, eventType);
|
|
}
|
|
}
|
|
|
|
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,
|
|
false,
|
|
null,
|
|
false,
|
|
null,
|
|
'',
|
|
null,
|
|
);
|
|
roots.set(containerTag, root);
|
|
}
|
|
updateContainer(element, root, null, callback);
|
|
|
|
// $FlowIssue Flow has hardcoded values for React DOM that don't work with RN
|
|
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,
|
|
) {
|
|
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();
|
|
|
|
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,
|
|
};
|
|
|
|
injectIntoDevTools({
|
|
findFiberByHostInstance: getClosestInstanceFromNode,
|
|
bundleType: __DEV__ ? 1 : 0,
|
|
version: ReactVersion,
|
|
rendererPackageName: 'react-native-renderer',
|
|
rendererConfig: {
|
|
getInspectorDataForViewTag: getInspectorDataForViewTag,
|
|
getInspectorDataForViewAtPoint: getInspectorDataForViewAtPoint.bind(
|
|
null,
|
|
findNodeHandle,
|
|
),
|
|
},
|
|
});
|