');
+ if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
+ expect(ref.current).not.toBe(span);
+ } else {
+ expect(ref.current).toBe(span);
+ }
+ });
+
+ it('recovers with client render when server rendered additional nodes deep inside suspense root', async () => {
+ const ref = React.createRef();
+ function App({hasB}) {
+ return (
+
);
+
+ const container = document.createElement('div');
+ container.innerHTML = finalHTML;
+
+ const span = container.getElementsByTagName('span')[0];
+
+ expect(container.innerHTML).toContain('
');
+ expect(ref.current).toBe(null);
+
+ expect(() => {
+ act(() => {
+ ReactDOM.hydrateRoot(container,
in ');
+
+ expect(container.innerHTML).toContain('A');
+ expect(container.innerHTML).not.toContain('B');
+ if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
+ expect(ref.current).not.toBe(span);
+ } else {
+ expect(ref.current).toBe(span);
+ }
});
it('calls the onDeleted hydration callback if the parent gets deleted', async () => {
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
index 91c564dd465ba..dd4bde63336e6 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
@@ -29,7 +29,10 @@ import type {
import type {SuspenseContext} from './ReactFiberSuspenseContext.new';
import type {OffscreenState} from './ReactFiberOffscreenComponent';
import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.new';
-import {enableSuspenseAvoidThisFallback} from 'shared/ReactFeatureFlags';
+import {
+ enableClientRenderFallbackOnHydrationMismatch,
+ enableSuspenseAvoidThisFallback,
+} from 'shared/ReactFeatureFlags';
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';
@@ -74,6 +77,9 @@ import {
StaticMask,
MutationMask,
Passive,
+ Incomplete,
+ ShouldCapture,
+ ForceClientRender,
} from './ReactFiberFlags';
import {
@@ -120,9 +126,11 @@ import {
prepareToHydrateHostInstance,
prepareToHydrateHostTextInstance,
prepareToHydrateHostSuspenseInstance,
+ warnDeleteNextHydratableInstance,
popHydrationState,
resetHydrationState,
getIsHydrating,
+ hasUnhydratedTailNodes,
} from './ReactFiberHydrationContext.new';
import {
enableSuspenseCallback,
@@ -1021,6 +1029,18 @@ function completeWork(
const nextState: null | SuspenseState = workInProgress.memoizedState;
if (enableSuspenseServerRenderer) {
+ if (
+ enableClientRenderFallbackOnHydrationMismatch &&
+ hasUnhydratedTailNodes() &&
+ (workInProgress.mode & ConcurrentMode) !== NoMode &&
+ (workInProgress.flags & DidCapture) === NoFlags
+ ) {
+ warnDeleteNextHydratableInstance(workInProgress);
+ resetHydrationState();
+ workInProgress.flags |=
+ ForceClientRender | Incomplete | ShouldCapture;
+ return workInProgress;
+ }
if (nextState !== null && nextState.dehydrated !== null) {
// We might be inside a hydration state the first time we're picking up this
// Suspense boundary, and also after we've reentered it for further hydration.
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
index ea994583cfe8d..63b51efbbb9b8 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
@@ -29,7 +29,10 @@ import type {
import type {SuspenseContext} from './ReactFiberSuspenseContext.old';
import type {OffscreenState} from './ReactFiberOffscreenComponent';
import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.old';
-import {enableSuspenseAvoidThisFallback} from 'shared/ReactFeatureFlags';
+import {
+ enableClientRenderFallbackOnHydrationMismatch,
+ enableSuspenseAvoidThisFallback,
+} from 'shared/ReactFeatureFlags';
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.old';
@@ -74,6 +77,9 @@ import {
StaticMask,
MutationMask,
Passive,
+ Incomplete,
+ ShouldCapture,
+ ForceClientRender,
} from './ReactFiberFlags';
import {
@@ -120,9 +126,11 @@ import {
prepareToHydrateHostInstance,
prepareToHydrateHostTextInstance,
prepareToHydrateHostSuspenseInstance,
+ warnDeleteNextHydratableInstance,
popHydrationState,
resetHydrationState,
getIsHydrating,
+ hasUnhydratedTailNodes,
} from './ReactFiberHydrationContext.old';
import {
enableSuspenseCallback,
@@ -1021,6 +1029,17 @@ function completeWork(
const nextState: null | SuspenseState = workInProgress.memoizedState;
if (enableSuspenseServerRenderer) {
+ if (
+ enableClientRenderFallbackOnHydrationMismatch &&
+ hasUnhydratedTailNodes() &&
+ (workInProgress.flags & DidCapture) === NoFlags
+ ) {
+ warnDeleteNextHydratableInstance(workInProgress);
+ resetHydrationState();
+ workInProgress.flags |=
+ ForceClientRender | Incomplete | ShouldCapture;
+ return workInProgress;
+ }
if (nextState !== null && nextState.dehydrated !== null) {
// We might be inside a hydration state the first time we're picking up this
// Suspense boundary, and also after we've reentered it for further hydration.
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
index 614c6c9d946ca..8989393936c9d 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
@@ -26,7 +26,13 @@ import {
HostRoot,
SuspenseComponent,
} from './ReactWorkTags';
-import {ChildDeletion, Placement, Hydrating} from './ReactFiberFlags';
+import {
+ ChildDeletion,
+ Placement,
+ Hydrating,
+ NoFlags,
+ DidCapture,
+} from './ReactFiberFlags';
import {
createFiberFromHostInstanceForDeletion,
@@ -121,7 +127,7 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
return true;
}
-function deleteHydratableInstance(
+function warnUnhydratedInstance(
returnFiber: Fiber,
instance: HydratableInstance,
) {
@@ -151,7 +157,13 @@ function deleteHydratableInstance(
break;
}
}
+}
+function deleteHydratableInstance(
+ returnFiber: Fiber,
+ instance: HydratableInstance,
+) {
+ warnUnhydratedInstance(returnFiber, instance);
const childToDelete = createFiberFromHostInstanceForDeletion();
childToDelete.stateNode = instance;
childToDelete.return = returnFiber;
@@ -330,7 +342,8 @@ function tryHydrate(fiber, nextInstance) {
function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) {
if (
enableClientRenderFallbackOnHydrationMismatch &&
- (fiber.mode & ConcurrentMode) !== NoMode
+ (fiber.mode & ConcurrentMode) !== NoMode &&
+ (fiber.flags & DidCapture) === NoFlags
) {
throw new Error(
'An error occurred during hydration. The server HTML was replaced with client content',
@@ -539,12 +552,15 @@ function popHydrationState(fiber: Fiber): boolean {
!shouldSetTextContent(fiber.type, fiber.memoizedProps)))
) {
let nextInstance = nextHydratableInstance;
- while (nextInstance) {
- deleteHydratableInstance(fiber, nextInstance);
- nextInstance = getNextHydratableSibling(nextInstance);
+ if (nextInstance) {
+ warnDeleteNextHydratableInstance(fiber);
+ throwOnHydrationMismatchIfConcurrentMode(fiber);
+ while (nextInstance) {
+ deleteHydratableInstance(fiber, nextInstance);
+ nextInstance = getNextHydratableSibling(nextInstance);
+ }
}
}
-
popToNextHostParent(fiber);
if (fiber.tag === SuspenseComponent) {
nextHydratableInstance = skipPastDehydratedSuspenseInstance(fiber);
@@ -556,6 +572,16 @@ function popHydrationState(fiber: Fiber): boolean {
return true;
}
+function hasUnhydratedTailNodes() {
+ return isHydrating && nextHydratableInstance !== null;
+}
+
+function warnDeleteNextHydratableInstance(fiber: Fiber) {
+ if (nextHydratableInstance) {
+ warnUnhydratedInstance(fiber, nextHydratableInstance);
+ }
+}
+
function resetHydrationState(): void {
if (!supportsHydration) {
return;
@@ -581,4 +607,6 @@ export {
prepareToHydrateHostTextInstance,
prepareToHydrateHostSuspenseInstance,
popHydrationState,
+ hasMore,
+ warnDeleteNextHydratableInstance,
};
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
index b7bca8217d979..cdd6a986b710a 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
@@ -26,7 +26,13 @@ import {
HostRoot,
SuspenseComponent,
} from './ReactWorkTags';
-import {ChildDeletion, Placement, Hydrating} from './ReactFiberFlags';
+import {
+ ChildDeletion,
+ Placement,
+ Hydrating,
+ NoFlags,
+ DidCapture,
+} from './ReactFiberFlags';
import {
createFiberFromHostInstanceForDeletion,
@@ -121,7 +127,7 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
return true;
}
-function deleteHydratableInstance(
+function warnUnhydratedInstance(
returnFiber: Fiber,
instance: HydratableInstance,
) {
@@ -151,7 +157,13 @@ function deleteHydratableInstance(
break;
}
}
+}
+function deleteHydratableInstance(
+ returnFiber: Fiber,
+ instance: HydratableInstance,
+) {
+ warnUnhydratedInstance(returnFiber, instance);
const childToDelete = createFiberFromHostInstanceForDeletion();
childToDelete.stateNode = instance;
childToDelete.return = returnFiber;
@@ -330,7 +342,8 @@ function tryHydrate(fiber, nextInstance) {
function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) {
if (
enableClientRenderFallbackOnHydrationMismatch &&
- (fiber.mode & ConcurrentMode) !== NoMode
+ (fiber.mode & ConcurrentMode) !== NoMode &&
+ (fiber.flags & DidCapture) === NoFlags
) {
throw new Error(
'An error occurred during hydration. The server HTML was replaced with client content',
@@ -539,12 +552,15 @@ function popHydrationState(fiber: Fiber): boolean {
!shouldSetTextContent(fiber.type, fiber.memoizedProps)))
) {
let nextInstance = nextHydratableInstance;
- while (nextInstance) {
- deleteHydratableInstance(fiber, nextInstance);
- nextInstance = getNextHydratableSibling(nextInstance);
+ if (nextInstance) {
+ warnDeleteNextHydratableInstance(fiber);
+ throwOnHydrationMismatchIfConcurrentMode(fiber);
+ while (nextInstance) {
+ deleteHydratableInstance(fiber, nextInstance);
+ nextInstance = getNextHydratableSibling(nextInstance);
+ }
}
}
-
popToNextHostParent(fiber);
if (fiber.tag === SuspenseComponent) {
nextHydratableInstance = skipPastDehydratedSuspenseInstance(fiber);
@@ -556,6 +572,16 @@ function popHydrationState(fiber: Fiber): boolean {
return true;
}
+function hasUnhydratedTailNodes() {
+ return isHydrating && nextHydratableInstance !== null;
+}
+
+function warnDeleteNextHydratableInstance(fiber: Fiber) {
+ if (nextHydratableInstance) {
+ warnUnhydratedInstance(fiber, nextHydratableInstance);
+ }
+}
+
function resetHydrationState(): void {
if (!supportsHydration) {
return;
@@ -581,4 +607,6 @@ export {
prepareToHydrateHostTextInstance,
prepareToHydrateHostSuspenseInstance,
popHydrationState,
+ hasUnhydratedTailNodes,
+ warnDeleteNextHydratableInstance,
};