From e6a1df0ac2fa896aa516684539760a186f115325 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 31 Jan 2022 12:50:19 -0500 Subject: [PATCH] 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. --- packages/react-art/src/ReactARTHostConfig.js | 2 +- .../src/__tests__/ReactDOMFizzServer-test.js | 66 ++++++++++++++++++- ...DOMServerPartialHydration-test.internal.js | 20 +++++- .../src/client/ReactDOMHostConfig.js | 10 +-- packages/react-dom/src/client/ReactDOMRoot.js | 17 +++-- .../src/ReactFabricHostConfig.js | 2 +- .../src/ReactNativeHostConfig.js | 2 +- .../src/createReactNoop.js | 2 +- .../src/ReactFiberThrow.new.js | 6 +- .../src/ReactFiberThrow.old.js | 6 +- .../src/ReactFiberWorkLoop.new.js | 50 +++++++++++--- .../src/ReactFiberWorkLoop.old.js | 50 +++++++++++--- .../useMutableSourceHydration-test.js | 39 ++++++++++- .../src/forks/ReactFiberHostConfig.custom.js | 2 +- .../src/ReactTestHostConfig.js | 2 +- 15 files changed, 226 insertions(+), 50 deletions(-) diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index b1f80ccc2be85..4ec8e3a4e6f2e 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -452,6 +452,6 @@ export function detachDeletedInstance(node: Instance): void { // noop } -export function logHydrationError(config, error) { +export function logRecoverableError(config, error) { // noop } diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index bcf0aaa461628..c2b474c1b1a40 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -1898,7 +1898,7 @@ describe('ReactDOMFizzServer', () => { // falls back to client rendering. isClient = true; ReactDOM.hydrateRoot(container, , { - onHydrationError(error) { + onRecoverableError(error) { Scheduler.unstable_yieldValue(error.message); }, }); @@ -1982,7 +1982,7 @@ describe('ReactDOMFizzServer', () => { // Hydrate the tree. Child will throw during render. isClient = true; ReactDOM.hydrateRoot(container, , { - onHydrationError(error) { + onRecoverableError(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. @@ -2063,7 +2063,7 @@ describe('ReactDOMFizzServer', () => { // falls back to client rendering. isClient = true; ReactDOM.hydrateRoot(container, , { - onHydrationError(error) { + onRecoverableError(error) { Scheduler.unstable_yieldValue(error.message); }, }); @@ -2100,4 +2100,64 @@ describe('ReactDOMFizzServer', () => { expect(span3Ref.current).toBe(span3); }, ); + + it('logs regular (non-hydration) errors when the UI recovers', async () => { + let shouldThrow = true; + + function A() { + if (shouldThrow) { + Scheduler.unstable_yieldValue('Oops!'); + throw new Error('Oops!'); + } + Scheduler.unstable_yieldValue('A'); + return 'A'; + } + + function B() { + Scheduler.unstable_yieldValue('B'); + return 'B'; + } + + function App() { + return ( + <> + + + + ); + } + + const root = ReactDOM.createRoot(container, { + onRecoverableError(error) { + Scheduler.unstable_yieldValue( + 'Logged a recoverable error: ' + error.message, + ); + }, + }); + React.startTransition(() => { + root.render(); + }); + + // Partially render A, but yield before the render has finished + expect(Scheduler).toFlushAndYieldThrough(['Oops!', 'Oops!']); + + // React will try rendering again synchronously. During the retry, A will + // not throw. This simulates a concurrent data race that is fixed by + // blocking the main thread. + shouldThrow = false; + expect(Scheduler).toFlushAndYield([ + // Finish initial render attempt + 'B', + + // Render again, synchronously + 'A', + 'B', + + // Log the error + 'Logged a recoverable error: Oops!', + ]); + + // UI looks normal + expect(container.textContent).toEqual('AB'); + }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 1a98b8e8f877e..a5ca716bdb96d 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -209,7 +209,7 @@ describe('ReactDOMServerPartialHydration', () => { // hydrating anyway. suspend = true; ReactDOM.hydrateRoot(container, , { - onHydrationError(error) { + onRecoverableError(error) { Scheduler.unstable_yieldValue(error.message); }, }); @@ -299,7 +299,7 @@ describe('ReactDOMServerPartialHydration', () => { client = true; ReactDOM.hydrateRoot(container, , { - onHydrationError(error) { + onRecoverableError(error) { Scheduler.unstable_yieldValue(error.message); }, }); @@ -3052,13 +3052,27 @@ describe('ReactDOMServerPartialHydration', () => { expect(() => { act(() => { - ReactDOM.hydrateRoot(container, ); + ReactDOM.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue( + 'Log recoverable error: ' + error.message, + ); + }, + }); }); }).toErrorDev( 'Warning: An error occurred during hydration. ' + 'The server HTML was replaced with client content in
.', {withoutStack: true}, ); + expect(Scheduler).toHaveYielded([ + 'Log recoverable error: An error occurred during hydration. The server ' + + 'HTML was replaced with client content', + // TODO: There were multiple mismatches in a single container. Should + // we attempt to de-dupe them? + 'Log recoverable error: An error occurred during hydration. The server ' + + 'HTML was replaced with client content', + ]); // We show fallback state when mismatch happens at root expect(container.innerHTML).toEqual( diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index cd2fad2fdb873..fdd8b38e312b7 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -379,15 +379,15 @@ export function getCurrentEventPriority(): * { return getEventPriority(currentEvent.type); } -export function logHydrationError( +export function logRecoverableError( config: ErrorLoggingConfig, error: mixed, ): void { - const onHydrationError = config; - if (onHydrationError !== null) { + const onRecoverableError = config; + if (onRecoverableError !== null) { // Schedule a callback to invoke the user-provided logging function. scheduleCallback(IdlePriority, () => { - onHydrationError(error); + onRecoverableError(error); }); } else { // Default behavior is to rethrow the error in a separate task. This will @@ -1094,6 +1094,8 @@ export function didNotFindHydratableSuspenseInstance( export function errorHydratingContainer(parentContainer: Container): void { if (__DEV__) { + // TODO: This gets logged by onRecoverableError, too, so we should be + // able to remove it. console.error( 'An error occurred during hydration. The server HTML was replaced with client content in <%s>.', parentContainer.nodeName.toLowerCase(), diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js index caf1f78c4801c..fe6b6ee31f773 100644 --- a/packages/react-dom/src/client/ReactDOMRoot.js +++ b/packages/react-dom/src/client/ReactDOMRoot.js @@ -24,6 +24,7 @@ export type CreateRootOptions = { unstable_strictMode?: boolean, unstable_concurrentUpdatesByDefault?: boolean, identifierPrefix?: string, + onRecoverableError?: (error: mixed) => void, ... }; @@ -36,7 +37,7 @@ export type HydrateRootOptions = { unstable_strictMode?: boolean, unstable_concurrentUpdatesByDefault?: boolean, identifierPrefix?: string, - onHydrationError?: (error: mixed) => void, + onRecoverableError?: (error: mixed) => void, ... }; @@ -144,6 +145,7 @@ export function createRoot( let isStrictMode = false; let concurrentUpdatesByDefaultOverride = false; let identifierPrefix = ''; + let onRecoverableError = null; if (options !== null && options !== undefined) { if (__DEV__) { if ((options: any).hydrate) { @@ -164,6 +166,9 @@ export function createRoot( if (options.identifierPrefix !== undefined) { identifierPrefix = options.identifierPrefix; } + if (options.onRecoverableError !== undefined) { + onRecoverableError = options.onRecoverableError; + } } const root = createContainer( @@ -174,7 +179,7 @@ export function createRoot( isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, - null, + onRecoverableError, ); markContainerAsRoot(root.current, container); @@ -215,7 +220,7 @@ export function hydrateRoot( let isStrictMode = false; let concurrentUpdatesByDefaultOverride = false; let identifierPrefix = ''; - let onHydrationError = null; + let onRecoverableError = null; if (options !== null && options !== undefined) { if (options.unstable_strictMode === true) { isStrictMode = true; @@ -229,8 +234,8 @@ export function hydrateRoot( if (options.identifierPrefix !== undefined) { identifierPrefix = options.identifierPrefix; } - if (options.onHydrationError !== undefined) { - onHydrationError = options.onHydrationError; + if (options.onRecoverableError !== undefined) { + onRecoverableError = options.onRecoverableError; } } @@ -242,7 +247,7 @@ export function hydrateRoot( isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, - onHydrationError, + onRecoverableError, ); 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/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 3d2f890387678..e720c2e12534e 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -528,7 +528,7 @@ export function detachDeletedInstance(node: Instance): void { // noop } -export function logHydrationError( +export function logRecoverableError( config: ErrorLoggingConfig, error: mixed, ): void { diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index bc7c859c4c858..27df360718aee 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -516,7 +516,7 @@ export function detachDeletedInstance(node: Instance): void { // noop } -export function logHydrationError( +export function logRecoverableError( config: ErrorLoggingConfig, error: mixed, ): void { diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index c93b5eb6e91dd..411757c0436be 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -467,7 +467,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { detachDeletedInstance() {}, - logHydrationError() { + logRecoverableError() { // no-op }, }; diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js index 60903d236e0d5..c2b2a9a2fa5e7 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.new.js +++ b/packages/react-reconciler/src/ReactFiberThrow.new.js @@ -37,7 +37,7 @@ import { import { supportsPersistence, getOffscreenContainerProps, - logHydrationError, + logRecoverableError, } from './ReactFiberHostConfig'; import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.new'; import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode'; @@ -515,7 +515,7 @@ function throwException( // 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); + logRecoverableError(root.errorLoggingConfig, value); return; } } else { @@ -526,7 +526,7 @@ function throwException( // We didn't find a boundary that could handle this type of exception. Start // over and traverse parent path again, this time treating the exception // as an error. - renderDidError(); + renderDidError(value); value = createCapturedValue(value, sourceFiber); let workInProgress = returnFiber; diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js index 6b7f4bf6055b4..3ae5df1f93414 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.old.js +++ b/packages/react-reconciler/src/ReactFiberThrow.old.js @@ -37,7 +37,7 @@ import { import { supportsPersistence, getOffscreenContainerProps, - logHydrationError, + logRecoverableError, } from './ReactFiberHostConfig'; import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.old'; import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode'; @@ -515,7 +515,7 @@ function throwException( // 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); + logRecoverableError(root.errorLoggingConfig, value); return; } } else { @@ -526,7 +526,7 @@ function throwException( // We didn't find a boundary that could handle this type of exception. Start // over and traverse parent path again, this time treating the exception // as an error. - renderDidError(); + renderDidError(value); value = createCapturedValue(value, sourceFiber); let workInProgress = returnFiber; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index b4b333547c194..f05174cf34f9c 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -76,6 +76,7 @@ import { supportsMicrotasks, errorHydratingContainer, scheduleMicrotask, + logRecoverableError, } from './ReactFiberHostConfig'; import { @@ -296,6 +297,7 @@ let workInProgressRootInterleavedUpdatedLanes: Lanes = NoLanes; let workInProgressRootRenderPhaseUpdatedLanes: Lanes = NoLanes; // Lanes that were pinged (in an interleaved event) during this render. let workInProgressRootPingedLanes: Lanes = NoLanes; +let workInProgressRootConcurrentErrors: Array | null = null; // The most recent time we committed a fallback. This lets us ensure a train // model where we don't commit new loading states in too quick succession. @@ -896,18 +898,40 @@ function recoverFromConcurrentError(root, errorRetryLanes) { let exitStatus; + let recoverableErrors = workInProgressRootConcurrentErrors; const MAX_ERROR_RETRY_ATTEMPTS = 50; for (let i = 0; i < MAX_ERROR_RETRY_ATTEMPTS; i++) { exitStatus = renderRootSync(root, errorRetryLanes); - if ( - exitStatus === RootErrored && - workInProgressRootRenderPhaseUpdatedLanes !== NoLanes - ) { - // There was a render phase update during this render. Some internal React - // implementation details may use this as a trick to schedule another - // render pass. To protect against an inifinite loop, eventually - // we'll give up. - continue; + if (exitStatus !== RootErrored) { + // Successfully finished rendering + if (recoverableErrors !== null) { + // Although we recovered the UI without surfacing an error, we should + // still log the errors so they can be fixed. + for (let j = 0; j < recoverableErrors.length; j++) { + const recoverableError = recoverableErrors[j]; + logRecoverableError(root.errorLoggingConfig, recoverableError); + } + } + } else { + // The UI failed to recover. + if (workInProgressRootRenderPhaseUpdatedLanes !== NoLanes) { + // There was a render phase update during this render. Some internal React + // implementation details may use this as a trick to schedule another + // render pass. To protect against an inifinite loop, eventually + // we'll give up. + // + // Add the newly thrown errors to the list of recoverable errors. If we + // eventually recover, we'll log them. Otherwise, we'll surface the + // error to the UI. + if (workInProgressRootConcurrentErrors !== null) { + if (recoverableErrors === null) { + recoverableErrors = workInProgressRootConcurrentErrors; + } else { + recoverableErrors.concat(workInProgressRootConcurrentErrors); + } + } + continue; + } } break; } @@ -1336,6 +1360,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) { workInProgressRootInterleavedUpdatedLanes = NoLanes; workInProgressRootRenderPhaseUpdatedLanes = NoLanes; workInProgressRootPingedLanes = NoLanes; + workInProgressRootConcurrentErrors = null; enqueueInterleavedUpdates(); @@ -1490,10 +1515,15 @@ export function renderDidSuspendDelayIfPossible(): void { } } -export function renderDidError() { +export function renderDidError(error: mixed) { if (workInProgressRootExitStatus !== RootSuspendedWithDelay) { workInProgressRootExitStatus = RootErrored; } + if (workInProgressRootConcurrentErrors === null) { + workInProgressRootConcurrentErrors = [error]; + } else { + workInProgressRootConcurrentErrors.push(error); + } } // Called during render to determine if anything has suspended. diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index d8bb61af50c84..7bc8edc872f96 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -76,6 +76,7 @@ import { supportsMicrotasks, errorHydratingContainer, scheduleMicrotask, + logRecoverableError, } from './ReactFiberHostConfig'; import { @@ -296,6 +297,7 @@ let workInProgressRootInterleavedUpdatedLanes: Lanes = NoLanes; let workInProgressRootRenderPhaseUpdatedLanes: Lanes = NoLanes; // Lanes that were pinged (in an interleaved event) during this render. let workInProgressRootPingedLanes: Lanes = NoLanes; +let workInProgressRootConcurrentErrors: Array | null = null; // The most recent time we committed a fallback. This lets us ensure a train // model where we don't commit new loading states in too quick succession. @@ -896,18 +898,40 @@ function recoverFromConcurrentError(root, errorRetryLanes) { let exitStatus; + let recoverableErrors = workInProgressRootConcurrentErrors; const MAX_ERROR_RETRY_ATTEMPTS = 50; for (let i = 0; i < MAX_ERROR_RETRY_ATTEMPTS; i++) { exitStatus = renderRootSync(root, errorRetryLanes); - if ( - exitStatus === RootErrored && - workInProgressRootRenderPhaseUpdatedLanes !== NoLanes - ) { - // There was a render phase update during this render. Some internal React - // implementation details may use this as a trick to schedule another - // render pass. To protect against an inifinite loop, eventually - // we'll give up. - continue; + if (exitStatus !== RootErrored) { + // Successfully finished rendering + if (recoverableErrors !== null) { + // Although we recovered the UI without surfacing an error, we should + // still log the errors so they can be fixed. + for (let j = 0; j < recoverableErrors.length; j++) { + const recoverableError = recoverableErrors[j]; + logRecoverableError(root.errorLoggingConfig, recoverableError); + } + } + } else { + // The UI failed to recover. + if (workInProgressRootRenderPhaseUpdatedLanes !== NoLanes) { + // There was a render phase update during this render. Some internal React + // implementation details may use this as a trick to schedule another + // render pass. To protect against an inifinite loop, eventually + // we'll give up. + // + // Add the newly thrown errors to the list of recoverable errors. If we + // eventually recover, we'll log them. Otherwise, we'll surface the + // error to the UI. + if (workInProgressRootConcurrentErrors !== null) { + if (recoverableErrors === null) { + recoverableErrors = workInProgressRootConcurrentErrors; + } else { + recoverableErrors.concat(workInProgressRootConcurrentErrors); + } + } + continue; + } } break; } @@ -1336,6 +1360,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) { workInProgressRootInterleavedUpdatedLanes = NoLanes; workInProgressRootRenderPhaseUpdatedLanes = NoLanes; workInProgressRootPingedLanes = NoLanes; + workInProgressRootConcurrentErrors = null; enqueueInterleavedUpdates(); @@ -1490,10 +1515,15 @@ export function renderDidSuspendDelayIfPossible(): void { } } -export function renderDidError() { +export function renderDidError(error: mixed) { if (workInProgressRootExitStatus !== RootSuspendedWithDelay) { workInProgressRootExitStatus = RootErrored; } + if (workInProgressRootConcurrentErrors === null) { + workInProgressRootConcurrentErrors = [error]; + } else { + workInProgressRootConcurrentErrors.push(error); + } } // Called during render to determine if anything has suspended. diff --git a/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js b/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js index 4c13ef7c35dfa..16ebb4657d28d 100644 --- a/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js +++ b/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js @@ -205,6 +205,9 @@ describe('useMutableSourceHydration', () => { act(() => { ReactDOM.hydrateRoot(container, , { mutableSources: [mutableSource], + onRecoverableError(error) { + Scheduler.unstable_yieldValue('Log error: ' + error.message); + }, }); source.value = 'two'; @@ -254,11 +257,17 @@ describe('useMutableSourceHydration', () => { React.startTransition(() => { ReactDOM.hydrateRoot(container, , { mutableSources: [mutableSource], + onRecoverableError(error) { + Scheduler.unstable_yieldValue('Log error: ' + error.message); + }, }); }); } else { ReactDOM.hydrateRoot(container, , { mutableSources: [mutableSource], + onRecoverableError(error) { + Scheduler.unstable_yieldValue('Log error: ' + error.message); + }, }); } expect(Scheduler).toFlushAndYieldThrough(['a:one']); @@ -269,7 +278,17 @@ describe('useMutableSourceHydration', () => { 'The server HTML was replaced with client content in
.', {withoutStack: true}, ); - expect(Scheduler).toHaveYielded(['a:two', 'b:two']); + expect(Scheduler).toHaveYielded([ + 'a:two', + 'b:two', + // TODO: Before onRecoverableError, this error was never surfaced to the + // user. The request to file an bug report no longer makes sense. + // However, the experimental useMutableSource API is slated for + // removal, anyway. + 'Log error: Cannot read from mutable source during the current ' + + 'render without tearing. This may be a bug in React. Please file ' + + 'an issue.', + ]); expect(source.listenerCount).toBe(2); }); @@ -328,11 +347,17 @@ describe('useMutableSourceHydration', () => { React.startTransition(() => { ReactDOM.hydrateRoot(container, fragment, { mutableSources: [mutableSource], + onRecoverableError(error) { + Scheduler.unstable_yieldValue('Log error: ' + error.message); + }, }); }); } else { ReactDOM.hydrateRoot(container, fragment, { mutableSources: [mutableSource], + onRecoverableError(error) { + Scheduler.unstable_yieldValue('Log error: ' + error.message); + }, }); } expect(Scheduler).toFlushAndYieldThrough(['0:a:one']); @@ -343,7 +368,17 @@ describe('useMutableSourceHydration', () => { 'The server HTML was replaced with client content in
.', {withoutStack: true}, ); - expect(Scheduler).toHaveYielded(['0:a:one', '1:b:two']); + expect(Scheduler).toHaveYielded([ + '0:a:one', + '1:b:two', + // TODO: Before onRecoverableError, this error was never surfaced to the + // user. The request to file an bug report no longer makes sense. + // However, the experimental useMutableSource API is slated for + // removal, anyway. + 'Log error: Cannot read from mutable source during the current ' + + 'render without tearing. This may be a bug in React. Please file ' + + 'an issue.', + ]); }); // @gate !enableSyncDefaultUpdates diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js index 38cee5f94e11c..8e67bf5517e45 100644 --- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -69,7 +69,7 @@ export const prepareScopeUpdate = $$$hostConfig.preparePortalMount; export const getInstanceFromScope = $$$hostConfig.getInstanceFromScope; export const getCurrentEventPriority = $$$hostConfig.getCurrentEventPriority; export const detachDeletedInstance = $$$hostConfig.detachDeletedInstance; -export const logHydrationError = $$$hostConfig.logHydrationError; +export const logRecoverableError = $$$hostConfig.logRecoverableError; // ------------------- // Microtasks diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js index 5279fda0b43f6..266b2c06e58a1 100644 --- a/packages/react-test-renderer/src/ReactTestHostConfig.js +++ b/packages/react-test-renderer/src/ReactTestHostConfig.js @@ -317,7 +317,7 @@ export function detachDeletedInstance(node: Instance): void { // noop } -export function logHydrationError( +export function logRecoverableError( config: ErrorLoggingConfig, error: mixed, ): void {