diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index 47bced8a1274a..b1f80ccc2be85 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -451,3 +451,7 @@ export function preparePortalMount(portalInstance: any): void { export function detachDeletedInstance(node: Instance): void { // noop } + +export function logHydrationError(config, error) { + // noop +} diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index d8db609f3f2cf..bcf0aaa461628 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -1897,9 +1897,15 @@ describe('ReactDOMFizzServer', () => { // Hydrate the tree. Child will throw during hydration, but not when it // falls back to client rendering. isClient = true; - ReactDOM.hydrateRoot(container, ); + ReactDOM.hydrateRoot(container, , { + onHydrationError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); - expect(Scheduler).toFlushAndYield(['Yay!']); + // An error logged but instead of surfacing it to the UI, we switched + // to client rendering. + expect(Scheduler).toFlushAndYield(['Yay!', 'Hydration error']); expect(getVisibleChildren(container)).toEqual(
@@ -1975,9 +1981,16 @@ describe('ReactDOMFizzServer', () => { // Hydrate the tree. Child will throw during render. isClient = true; - ReactDOM.hydrateRoot(container, ); + ReactDOM.hydrateRoot(container, , { + onHydrationError(error) { + // TODO: We logged a hydration error, but the same error ends up + // being thrown during the fallback to client rendering, too. Maybe + // we should only log if the client render succeeds. + Scheduler.unstable_yieldValue(error.message); + }, + }); - expect(Scheduler).toFlushAndYield([]); + expect(Scheduler).toFlushAndYield(['Oops!']); expect(getVisibleChildren(container)).toEqual('Oops!'); }, ); @@ -2049,9 +2062,15 @@ describe('ReactDOMFizzServer', () => { // Hydrate the tree. Child will throw during hydration, but not when it // falls back to client rendering. isClient = true; - ReactDOM.hydrateRoot(container, ); + ReactDOM.hydrateRoot(container, , { + onHydrationError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); - expect(Scheduler).toFlushAndYield([]); + // An error logged but instead of surfacing it to the UI, we switched + // to client rendering. + expect(Scheduler).toFlushAndYield(['Hydration error']); expect(getVisibleChildren(container)).toEqual(
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 1c65501f7b382..1a98b8e8f877e 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -208,9 +208,17 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - ReactDOM.hydrateRoot(container, ); + ReactDOM.hydrateRoot(container, , { + onHydrationError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) { - Scheduler.unstable_flushAll(); + // Hydration error is logged + expect(Scheduler).toFlushAndYield([ + 'An error occurred during hydration. The server HTML was replaced ' + + 'with client content', + ]); } else { expect(() => { Scheduler.unstable_flushAll(); @@ -290,13 +298,24 @@ describe('ReactDOMServerPartialHydration', () => { suspend = true; client = true; - ReactDOM.hydrateRoot(container, ); + ReactDOM.hydrateRoot(container, , { + onHydrationError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); expect(Scheduler).toFlushAndYield([ 'Suspend', 'Component', 'Component', 'Component', 'Component', + + // Hydration mismatch errors are logged. + // TODO: This could get noisy. Is there some way to dedupe? + 'An error occurred during hydration. The server HTML was replaced with client content', + 'An error occurred during hydration. The server HTML was replaced with client content', + 'An error occurred during hydration. The server HTML was replaced with client content', + 'An error occurred during hydration. The server HTML was replaced with client content', ]); jest.runAllTimers(); @@ -316,12 +335,16 @@ describe('ReactDOMServerPartialHydration', () => { 'Component', 'Component', 'Component', + // second pass as client render 'Hello', 'Component', 'Component', 'Component', 'Component', + + // Hydration mismatch is logged + 'An error occurred during hydration. The server HTML was replaced with client content', ]); // Client rendered - suspense comment nodes removed diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 341d1fa7a3764..cd2fad2fdb873 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -70,6 +70,7 @@ import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags'; import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem'; import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities'; +import {scheduleCallback, IdlePriority} from 'react-reconciler/src/Scheduler'; export type Type = string; export type Props = { @@ -123,6 +124,10 @@ export type TimeoutHandle = TimeoutID; export type NoTimeout = -1; export type RendererInspectionConfig = $ReadOnly<{||}>; +// Right now this is a single callback, but could be multiple in the in the +// future. +export type ErrorLoggingConfig = null | ((error: mixed) => void); + type SelectionInformation = {| focusedElem: null | HTMLElement, selectionRange: mixed, @@ -374,6 +379,25 @@ export function getCurrentEventPriority(): * { return getEventPriority(currentEvent.type); } +export function logHydrationError( + config: ErrorLoggingConfig, + error: mixed, +): void { + const onHydrationError = config; + if (onHydrationError !== null) { + // Schedule a callback to invoke the user-provided logging function. + scheduleCallback(IdlePriority, () => { + onHydrationError(error); + }); + } else { + // Default behavior is to rethrow the error in a separate task. This will + // trigger a browser error event. + scheduleCallback(IdlePriority, () => { + throw error; + }); + } +} + export const isPrimaryRenderer = true; export const warnsIfNotActing = true; // This initialization code may run even on server environments diff --git a/packages/react-dom/src/client/ReactDOMLegacy.js b/packages/react-dom/src/client/ReactDOMLegacy.js index 05ae0d5ce4f45..cb95101401ea4 100644 --- a/packages/react-dom/src/client/ReactDOMLegacy.js +++ b/packages/react-dom/src/client/ReactDOMLegacy.js @@ -122,6 +122,7 @@ function legacyCreateRootFromDOMContainer( false, // isStrictMode false, // concurrentUpdatesByDefaultOverride, '', // identifierPrefix + null, ); markContainerAsRoot(root.current, container); diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js index 800eeba0d018d..caf1f78c4801c 100644 --- a/packages/react-dom/src/client/ReactDOMRoot.js +++ b/packages/react-dom/src/client/ReactDOMRoot.js @@ -36,6 +36,7 @@ export type HydrateRootOptions = { unstable_strictMode?: boolean, unstable_concurrentUpdatesByDefault?: boolean, identifierPrefix?: string, + onHydrationError?: (error: mixed) => void, ... }; @@ -173,6 +174,7 @@ export function createRoot( isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, + null, ); markContainerAsRoot(root.current, container); @@ -213,6 +215,7 @@ export function hydrateRoot( let isStrictMode = false; let concurrentUpdatesByDefaultOverride = false; let identifierPrefix = ''; + let onHydrationError = null; if (options !== null && options !== undefined) { if (options.unstable_strictMode === true) { isStrictMode = true; @@ -226,6 +229,9 @@ export function hydrateRoot( if (options.identifierPrefix !== undefined) { identifierPrefix = options.identifierPrefix; } + if (options.onHydrationError !== undefined) { + onHydrationError = options.onHydrationError; + } } const root = createContainer( @@ -236,6 +242,7 @@ export function hydrateRoot( isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, + onHydrationError, ); markContainerAsRoot(root.current, container); // This can't be a comment node since hydration doesn't work on comment nodes anyway. diff --git a/packages/react-native-renderer/src/ReactFabric.js b/packages/react-native-renderer/src/ReactFabric.js index bf7754d6099c2..25cbd31b9fdf5 100644 --- a/packages/react-native-renderer/src/ReactFabric.js +++ b/packages/react-native-renderer/src/ReactFabric.js @@ -214,6 +214,7 @@ function render( false, null, '', + null, ); roots.set(containerTag, root); } diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 727b782efd768..3d2f890387678 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -95,6 +95,8 @@ export type RendererInspectionConfig = $ReadOnly<{| ) => void, |}>; +export type ErrorLoggingConfig = null; + // TODO: Remove this conditional once all changes have propagated. if (registerEventHandler) { /** @@ -525,3 +527,10 @@ export function preparePortalMount(portalInstance: Instance): void { export function detachDeletedInstance(node: Instance): void { // noop } + +export function logHydrationError( + config: ErrorLoggingConfig, + error: mixed, +): void { + // noop +} diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index 10c5e37f41bcc..bc7c859c4c858 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -55,6 +55,8 @@ export type RendererInspectionConfig = $ReadOnly<{| ) => void, |}>; +export type ErrorLoggingConfig = null; + const UPDATE_SIGNAL = {}; if (__DEV__) { Object.freeze(UPDATE_SIGNAL); @@ -513,3 +515,10 @@ export function preparePortalMount(portalInstance: Instance): void { export function detachDeletedInstance(node: Instance): void { // noop } + +export function logHydrationError( + config: ErrorLoggingConfig, + error: mixed, +): void { + // noop +} diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js index fb539d8996811..c5d4318311c0b 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -210,6 +210,7 @@ function render( false, null, '', + null, ); roots.set(containerTag, root); } diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index ef76b6610617f..c93b5eb6e91dd 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -466,6 +466,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { }, detachDeletedInstance() {}, + + logHydrationError() { + // no-op + }, }; const hostConfig = useMutation diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index 1cf200e35d56d..93a3e13bef85a 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -15,6 +15,7 @@ import type { TextInstance, Container, PublicInstance, + ErrorLoggingConfig, } from './ReactFiberHostConfig'; import type {RendererInspectionConfig} from './ReactFiberHostConfig'; import type {ReactNodeList} from 'shared/ReactTypes'; @@ -245,6 +246,7 @@ export function createContainer( isStrictMode: boolean, concurrentUpdatesByDefaultOverride: null | boolean, identifierPrefix: string, + errorLoggingConfig: ErrorLoggingConfig, ): OpaqueRoot { return createFiberRoot( containerInfo, @@ -254,6 +256,7 @@ export function createContainer( isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, + errorLoggingConfig, ); } diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index 4b02959ab0840..0fcba6293c7b7 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -15,6 +15,7 @@ import type { TextInstance, Container, PublicInstance, + ErrorLoggingConfig, } from './ReactFiberHostConfig'; import type {RendererInspectionConfig} from './ReactFiberHostConfig'; import type {ReactNodeList} from 'shared/ReactTypes'; @@ -245,6 +246,7 @@ export function createContainer( isStrictMode: boolean, concurrentUpdatesByDefaultOverride: null | boolean, identifierPrefix: string, + errorLoggingConfig: ErrorLoggingConfig, ): OpaqueRoot { return createFiberRoot( containerInfo, @@ -254,6 +256,7 @@ export function createContainer( isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, + errorLoggingConfig, ); } diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js index 9e9feb45d9b03..dd5e6a5fc7da0 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.new.js +++ b/packages/react-reconciler/src/ReactFiberRoot.new.js @@ -9,6 +9,7 @@ import type {FiberRoot, SuspenseHydrationCallbacks} from './ReactInternalTypes'; import type {RootTag} from './ReactRootTags'; +import type {ErrorLoggingConfig} from './ReactFiberHostConfig'; import {noTimeout, supportsHydration} from './ReactFiberHostConfig'; import {createHostRootFiber} from './ReactFiber.new'; @@ -30,7 +31,13 @@ import {initializeUpdateQueue} from './ReactUpdateQueue.new'; import {LegacyRoot, ConcurrentRoot} from './ReactRootTags'; import {createCache, retainCache} from './ReactFiberCacheComponent.new'; -function FiberRootNode(containerInfo, tag, hydrate, identifierPrefix) { +function FiberRootNode( + containerInfo, + tag, + hydrate, + identifierPrefix, + errorLoggingConfig, +) { this.tag = tag; this.containerInfo = containerInfo; this.pendingChildren = null; @@ -57,6 +64,7 @@ function FiberRootNode(containerInfo, tag, hydrate, identifierPrefix) { this.entanglements = createLaneMap(NoLanes); this.identifierPrefix = identifierPrefix; + this.errorLoggingConfig = errorLoggingConfig; if (enableCache) { this.pooledCache = null; @@ -103,13 +111,19 @@ export function createFiberRoot( hydrationCallbacks: null | SuspenseHydrationCallbacks, isStrictMode: boolean, concurrentUpdatesByDefaultOverride: null | boolean, + // TODO: We have several of these arguments that are conceptually part of the + // host config, but because they are passed in at runtime, we have to thread + // 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, + errorLoggingConfig: ErrorLoggingConfig, ): FiberRoot { const root: FiberRoot = (new FiberRootNode( containerInfo, tag, hydrate, identifierPrefix, + errorLoggingConfig, ): any); if (enableSuspenseCallback) { root.hydrationCallbacks = hydrationCallbacks; diff --git a/packages/react-reconciler/src/ReactFiberRoot.old.js b/packages/react-reconciler/src/ReactFiberRoot.old.js index d8d061297854f..d6791e97c34fd 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.old.js +++ b/packages/react-reconciler/src/ReactFiberRoot.old.js @@ -9,6 +9,7 @@ import type {FiberRoot, SuspenseHydrationCallbacks} from './ReactInternalTypes'; import type {RootTag} from './ReactRootTags'; +import type {ErrorLoggingConfig} from './ReactFiberHostConfig'; import {noTimeout, supportsHydration} from './ReactFiberHostConfig'; import {createHostRootFiber} from './ReactFiber.old'; @@ -30,7 +31,13 @@ import {initializeUpdateQueue} from './ReactUpdateQueue.old'; import {LegacyRoot, ConcurrentRoot} from './ReactRootTags'; import {createCache, retainCache} from './ReactFiberCacheComponent.old'; -function FiberRootNode(containerInfo, tag, hydrate, identifierPrefix) { +function FiberRootNode( + containerInfo, + tag, + hydrate, + identifierPrefix, + errorLoggingConfig, +) { this.tag = tag; this.containerInfo = containerInfo; this.pendingChildren = null; @@ -57,6 +64,7 @@ function FiberRootNode(containerInfo, tag, hydrate, identifierPrefix) { this.entanglements = createLaneMap(NoLanes); this.identifierPrefix = identifierPrefix; + this.errorLoggingConfig = errorLoggingConfig; if (enableCache) { this.pooledCache = null; @@ -103,13 +111,19 @@ export function createFiberRoot( hydrationCallbacks: null | SuspenseHydrationCallbacks, isStrictMode: boolean, concurrentUpdatesByDefaultOverride: null | boolean, + // TODO: We have several of these arguments that are conceptually part of the + // host config, but because they are passed in at runtime, we have to thread + // 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, + errorLoggingConfig: ErrorLoggingConfig, ): FiberRoot { const root: FiberRoot = (new FiberRootNode( containerInfo, tag, hydrate, identifierPrefix, + errorLoggingConfig, ): any); if (enableSuspenseCallback) { root.hydrationCallbacks = hydrationCallbacks; diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js index cd9931687ba5a..60903d236e0d5 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.new.js +++ b/packages/react-reconciler/src/ReactFiberThrow.new.js @@ -37,6 +37,7 @@ import { import { supportsPersistence, getOffscreenContainerProps, + logHydrationError, } from './ReactFiberHostConfig'; import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.new'; import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode'; @@ -507,6 +508,14 @@ function throwException( root, rootRenderLanes, ); + + // Even though the user may not be affected by this error, we should + // still log it so it can be fixed. + // TODO: For now, we only log errors that occur during hydration, but we + // probably want to log any error that is recovered from without + // triggering an error boundary — or maybe even those, too. Need to + // figure out the right API. + logHydrationError(root.errorLoggingConfig, value); return; } } else { diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js index 8f6d18a48dea3..6b7f4bf6055b4 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.old.js +++ b/packages/react-reconciler/src/ReactFiberThrow.old.js @@ -37,6 +37,7 @@ import { import { supportsPersistence, getOffscreenContainerProps, + logHydrationError, } from './ReactFiberHostConfig'; import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.old'; import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode'; @@ -507,6 +508,14 @@ function throwException( root, rootRenderLanes, ); + + // Even though the user may not be affected by this error, we should + // still log it so it can be fixed. + // TODO: For now, we only log errors that occur during hydration, but we + // probably want to log any error that is recovered from without + // triggering an error boundary — or maybe even those, too. Need to + // figure out the right API. + logHydrationError(root.errorLoggingConfig, value); return; } } else { diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index 13965720b7cd3..1cc2128356090 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -16,7 +16,10 @@ import type { MutableSourceVersion, MutableSource, } from 'shared/ReactTypes'; -import type {SuspenseInstance} from './ReactFiberHostConfig'; +import type { + SuspenseInstance, + ErrorLoggingConfig, +} from './ReactFiberHostConfig'; import type {WorkTag} from './ReactWorkTags'; import type {TypeOfMode} from './ReactTypeOfMode'; import type {Flags} from './ReactFiberFlags'; @@ -246,6 +249,8 @@ type BaseFiberRootProperties = {| // the public createRoot object, which the fiber tree does not currently have // a reference to. identifierPrefix: string, + + errorLoggingConfig: ErrorLoggingConfig, |}; // The following attributes are only used by DevTools and are only present in DEV builds. diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js index 4bf292df79f7a..d0c3d5b236ea4 100644 --- a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js @@ -76,6 +76,7 @@ describe('ReactFiberHostContext', () => { null, false, '', + null, ); act(() => { Renderer.updateContainer( @@ -139,6 +140,7 @@ describe('ReactFiberHostContext', () => { null, false, '', + null, ); act(() => { Renderer.updateContainer( diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js index 6535d8d3fdec3..2f86a13dc3535 100644 --- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -38,6 +38,7 @@ export opaque type ChildSet = mixed; // eslint-disable-line no-undef export opaque type TimeoutHandle = mixed; // eslint-disable-line no-undef export opaque type NoTimeout = mixed; // eslint-disable-line no-undef export opaque type RendererInspectionConfig = mixed; // eslint-disable-line no-undef +export opaque type ErrorCallbackConfig = mixed; // eslint-disable-line no-undef export type EventResponder = any; export const getPublicInstance = $$$hostConfig.getPublicInstance; @@ -68,6 +69,7 @@ export const prepareScopeUpdate = $$$hostConfig.preparePortalMount; export const getInstanceFromScope = $$$hostConfig.getInstanceFromScope; export const getCurrentEventPriority = $$$hostConfig.getCurrentEventPriority; export const detachDeletedInstance = $$$hostConfig.detachDeletedInstance; +export const onHydrationError = $$$hostConfig.errorHydratingContainer; // ------------------- // Microtasks diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js index 503b08efaf20b..5279fda0b43f6 100644 --- a/packages/react-test-renderer/src/ReactTestHostConfig.js +++ b/packages/react-test-renderer/src/ReactTestHostConfig.js @@ -42,6 +42,8 @@ export type EventResponder = any; export type RendererInspectionConfig = $ReadOnly<{||}>; +export type ErrorLoggingConfig = null; + export * from 'react-reconciler/src/ReactFiberHostConfigWithNoPersistence'; export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration'; export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors'; @@ -314,3 +316,10 @@ export function getInstanceFromScope(scopeInstance: Object): null | Object { export function detachDeletedInstance(node: Instance): void { // noop } + +export function logHydrationError( + config: ErrorLoggingConfig, + error: mixed, +): void { + // noop +} diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index de6e4beffec5f..a8121d1a14fcf 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -472,6 +472,7 @@ function create(element: React$Element, options: TestRendererOptions) { isStrictMode, concurrentUpdatesByDefault, '', + null, ); if (root == null) {