');
+
+ 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 91c564dd465ba5..21f86645735dbb 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,
+ hasMore,
} from './ReactFiberHydrationContext.new';
import {
enableSuspenseCallback,
@@ -828,7 +836,6 @@ function completeWork(
// to the current tree provider fiber is just as fast and less error-prone.
// Ideally we would have a special version of the work loop only
// for hydration.
- popTreeContext(workInProgress);
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
@@ -840,9 +847,11 @@ function completeWork(
case Profiler:
case ContextConsumer:
case MemoComponent:
+ popTreeContext(workInProgress);
bubbleProperties(workInProgress);
return null;
case ClassComponent: {
+ popTreeContext(workInProgress);
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
@@ -852,6 +861,23 @@ function completeWork(
}
case HostRoot: {
const fiberRoot = (workInProgress.stateNode: FiberRoot);
+ if (current === null || current.child === null) {
+ // If we hydrated, pop so that we can delete any remaining children
+ // that weren't hydrated.
+ const wasHydrated = popHydrationState(workInProgress);
+ if (wasHydrated) {
+ // If we hydrated, then we'll need to schedule an update for
+ // the commit side-effects on the root.
+ markUpdate(workInProgress);
+ } else if (!fiberRoot.isDehydrated) {
+ // Schedule an effect to clear this container at the start of the next commit.
+ // This handles the case of React rendering into a container with previous children.
+ // It's also safe to do for updates too, because current.child would only be null
+ // if the previous render was null (so the container would already be empty).
+ workInProgress.flags |= Snapshot;
+ }
+ }
+ popTreeContext(workInProgress);
if (enableCache) {
popRootCachePool(fiberRoot, renderLanes);
@@ -873,27 +899,13 @@ function completeWork(
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
}
- if (current === null || current.child === null) {
- // If we hydrated, pop so that we can delete any remaining children
- // that weren't hydrated.
- const wasHydrated = popHydrationState(workInProgress);
- if (wasHydrated) {
- // If we hydrated, then we'll need to schedule an update for
- // the commit side-effects on the root.
- markUpdate(workInProgress);
- } else if (!fiberRoot.isDehydrated) {
- // Schedule an effect to clear this container at the start of the next commit.
- // This handles the case of React rendering into a container with previous children.
- // It's also safe to do for updates too, because current.child would only be null
- // if the previous render was null (so the container would already be empty).
- workInProgress.flags |= Snapshot;
- }
- }
updateHostContainer(current, workInProgress);
bubbleProperties(workInProgress);
return null;
}
case HostComponent: {
+ const wasHydrated = popHydrationState(workInProgress);
+ popTreeContext(workInProgress);
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
@@ -928,7 +940,6 @@ function completeWork(
// "stack" as the parent. Then append children as we go in beginWork
// or completeWork depending on whether we want to add them top->down or
// bottom->up. Top->down is faster in IE11.
- const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// TODO: Move this and createInstance step into the beginPhase
// to consolidate.
@@ -981,6 +992,7 @@ function completeWork(
return null;
}
case HostText: {
+ popTreeContext(workInProgress);
const newText = newProps;
if (current && workInProgress.stateNode != null) {
const oldText = current.memoizedProps;
@@ -1017,14 +1029,27 @@ function completeWork(
return null;
}
case SuspenseComponent: {
- popSuspenseContext(workInProgress);
const nextState: null | SuspenseState = workInProgress.memoizedState;
-
if (enableSuspenseServerRenderer) {
+ if (
+ enableClientRenderFallbackOnHydrationMismatch &&
+ hasMore() &&
+ (workInProgress.flags & DidCapture) === NoFlags
+ ) {
+ warnDeleteNextHydratableInstance(workInProgress);
+ resetHydrationState();
+ workInProgress.flags |=
+ ForceClientRender | Incomplete | ShouldCapture;
+ popTreeContext(workInProgress);
+ popSuspenseContext(workInProgress);
+ 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.
const wasHydrated = popHydrationState(workInProgress);
+ popTreeContext(workInProgress);
+ popSuspenseContext(workInProgress);
if (current === null) {
if (!wasHydrated) {
throw new Error(
@@ -1091,6 +1116,8 @@ function completeWork(
) {
transferActualDuration(workInProgress);
}
+ popTreeContext(workInProgress);
+ popSuspenseContext(workInProgress);
// Don't bubble properties in this case.
return workInProgress;
}
@@ -1103,6 +1130,8 @@ function completeWork(
const prevState: null | SuspenseState = current.memoizedState;
prevDidTimeout = prevState !== null;
}
+ popTreeContext(workInProgress);
+ popSuspenseContext(workInProgress);
if (enableCache && nextDidTimeout) {
const offscreenFiber: Fiber = (workInProgress.child: any);
@@ -1207,6 +1236,7 @@ function completeWork(
return null;
}
case HostPortal:
+ popTreeContext(workInProgress);
popHostContainer(workInProgress);
updateHostContainer(current, workInProgress);
if (current === null) {
@@ -1215,12 +1245,14 @@ function completeWork(
bubbleProperties(workInProgress);
return null;
case ContextProvider:
+ popTreeContext(workInProgress);
// Pop provider fiber
const context: ReactContext
= workInProgress.type._context;
popProvider(context, workInProgress);
bubbleProperties(workInProgress);
return null;
case IncompleteClassComponent: {
+ popTreeContext(workInProgress);
// Same as class component case. I put it down here so that the tags are
// sequential to ensure this switch is compiled to a jump table.
const Component = workInProgress.type;
@@ -1231,6 +1263,7 @@ function completeWork(
return null;
}
case SuspenseListComponent: {
+ popTreeContext(workInProgress);
popSuspenseContext(workInProgress);
const renderState: null | SuspenseListRenderState =
@@ -1440,6 +1473,7 @@ function completeWork(
return null;
}
case ScopeComponent: {
+ popTreeContext(workInProgress);
if (enableScopeAPI) {
if (current === null) {
const scopeInstance: ReactScopeInstance = createScopeInstance();
@@ -1464,6 +1498,7 @@ function completeWork(
}
case OffscreenComponent:
case LegacyHiddenComponent: {
+ popTreeContext(workInProgress);
popRenderLanes(workInProgress);
const nextState: OffscreenState | null = workInProgress.memoizedState;
const nextIsHidden = nextState !== null;
@@ -1532,6 +1567,7 @@ function completeWork(
return null;
}
case CacheComponent: {
+ popTreeContext(workInProgress);
if (enableCache) {
let previousCache: Cache | null = null;
if (workInProgress.alternate !== null) {
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
index ea994583cfe8dd..9830f51a10fa7a 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,
+ hasMore,
} from './ReactFiberHydrationContext.old';
import {
enableSuspenseCallback,
@@ -828,7 +836,6 @@ function completeWork(
// to the current tree provider fiber is just as fast and less error-prone.
// Ideally we would have a special version of the work loop only
// for hydration.
- popTreeContext(workInProgress);
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
@@ -840,9 +847,11 @@ function completeWork(
case Profiler:
case ContextConsumer:
case MemoComponent:
+ popTreeContext(workInProgress);
bubbleProperties(workInProgress);
return null;
case ClassComponent: {
+ popTreeContext(workInProgress);
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
@@ -852,6 +861,23 @@ function completeWork(
}
case HostRoot: {
const fiberRoot = (workInProgress.stateNode: FiberRoot);
+ if (current === null || current.child === null) {
+ // If we hydrated, pop so that we can delete any remaining children
+ // that weren't hydrated.
+ const wasHydrated = popHydrationState(workInProgress);
+ if (wasHydrated) {
+ // If we hydrated, then we'll need to schedule an update for
+ // the commit side-effects on the root.
+ markUpdate(workInProgress);
+ } else if (!fiberRoot.isDehydrated) {
+ // Schedule an effect to clear this container at the start of the next commit.
+ // This handles the case of React rendering into a container with previous children.
+ // It's also safe to do for updates too, because current.child would only be null
+ // if the previous render was null (so the container would already be empty).
+ workInProgress.flags |= Snapshot;
+ }
+ }
+ popTreeContext(workInProgress);
if (enableCache) {
popRootCachePool(fiberRoot, renderLanes);
@@ -873,27 +899,13 @@ function completeWork(
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
}
- if (current === null || current.child === null) {
- // If we hydrated, pop so that we can delete any remaining children
- // that weren't hydrated.
- const wasHydrated = popHydrationState(workInProgress);
- if (wasHydrated) {
- // If we hydrated, then we'll need to schedule an update for
- // the commit side-effects on the root.
- markUpdate(workInProgress);
- } else if (!fiberRoot.isDehydrated) {
- // Schedule an effect to clear this container at the start of the next commit.
- // This handles the case of React rendering into a container with previous children.
- // It's also safe to do for updates too, because current.child would only be null
- // if the previous render was null (so the container would already be empty).
- workInProgress.flags |= Snapshot;
- }
- }
updateHostContainer(current, workInProgress);
bubbleProperties(workInProgress);
return null;
}
case HostComponent: {
+ const wasHydrated = popHydrationState(workInProgress);
+ popTreeContext(workInProgress);
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
@@ -928,7 +940,6 @@ function completeWork(
// "stack" as the parent. Then append children as we go in beginWork
// or completeWork depending on whether we want to add them top->down or
// bottom->up. Top->down is faster in IE11.
- const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// TODO: Move this and createInstance step into the beginPhase
// to consolidate.
@@ -981,6 +992,7 @@ function completeWork(
return null;
}
case HostText: {
+ popTreeContext(workInProgress);
const newText = newProps;
if (current && workInProgress.stateNode != null) {
const oldText = current.memoizedProps;
@@ -1017,14 +1029,23 @@ function completeWork(
return null;
}
case SuspenseComponent: {
- popSuspenseContext(workInProgress);
const nextState: null | SuspenseState = workInProgress.memoizedState;
-
if (enableSuspenseServerRenderer) {
if (nextState !== null && nextState.dehydrated !== null) {
+ if (enableClientRenderFallbackOnHydrationMismatch && hasMore()) {
+ warnDeleteNextHydratableInstance(workInProgress);
+ resetHydrationState();
+ workInProgress.flags |=
+ ForceClientRender | Incomplete | ShouldCapture;
+ popTreeContext(workInProgress);
+ popSuspenseContext(workInProgress);
+ return workInProgress;
+ }
// 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.
const wasHydrated = popHydrationState(workInProgress);
+ popTreeContext(workInProgress);
+ popSuspenseContext(workInProgress);
if (current === null) {
if (!wasHydrated) {
throw new Error(
@@ -1091,6 +1112,8 @@ function completeWork(
) {
transferActualDuration(workInProgress);
}
+ popTreeContext(workInProgress);
+ popSuspenseContext(workInProgress);
// Don't bubble properties in this case.
return workInProgress;
}
@@ -1104,6 +1127,9 @@ function completeWork(
prevDidTimeout = prevState !== null;
}
+ popTreeContext(workInProgress);
+ popSuspenseContext(workInProgress);
+
if (enableCache && nextDidTimeout) {
const offscreenFiber: Fiber = (workInProgress.child: any);
let previousCache: Cache | null = null;
@@ -1207,6 +1233,7 @@ function completeWork(
return null;
}
case HostPortal:
+ popTreeContext(workInProgress);
popHostContainer(workInProgress);
updateHostContainer(current, workInProgress);
if (current === null) {
@@ -1215,12 +1242,14 @@ function completeWork(
bubbleProperties(workInProgress);
return null;
case ContextProvider:
+ popTreeContext(workInProgress);
// Pop provider fiber
const context: ReactContext = workInProgress.type._context;
popProvider(context, workInProgress);
bubbleProperties(workInProgress);
return null;
case IncompleteClassComponent: {
+ popTreeContext(workInProgress);
// Same as class component case. I put it down here so that the tags are
// sequential to ensure this switch is compiled to a jump table.
const Component = workInProgress.type;
@@ -1231,6 +1260,7 @@ function completeWork(
return null;
}
case SuspenseListComponent: {
+ popTreeContext(workInProgress);
popSuspenseContext(workInProgress);
const renderState: null | SuspenseListRenderState =
@@ -1440,6 +1470,7 @@ function completeWork(
return null;
}
case ScopeComponent: {
+ popTreeContext(workInProgress);
if (enableScopeAPI) {
if (current === null) {
const scopeInstance: ReactScopeInstance = createScopeInstance();
@@ -1464,6 +1495,7 @@ function completeWork(
}
case OffscreenComponent:
case LegacyHiddenComponent: {
+ popTreeContext(workInProgress);
popRenderLanes(workInProgress);
const nextState: OffscreenState | null = workInProgress.memoizedState;
const nextIsHidden = nextState !== null;
@@ -1532,6 +1564,7 @@ function completeWork(
return null;
}
case CacheComponent: {
+ popTreeContext(workInProgress);
if (enableCache) {
let previousCache: Cache | null = null;
if (workInProgress.alternate !== null) {
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
index 614c6c9d946ca9..582a39f756e1fa 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
@@ -121,7 +121,7 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
return true;
}
-function deleteHydratableInstance(
+function warnDeleteHydratableInstance(
returnFiber: Fiber,
instance: HydratableInstance,
) {
@@ -151,7 +151,13 @@ function deleteHydratableInstance(
break;
}
}
+}
+function deleteHydratableInstance(
+ returnFiber: Fiber,
+ instance: HydratableInstance,
+) {
+ warnDeleteHydratableInstance(returnFiber, instance);
const childToDelete = createFiberFromHostInstanceForDeletion();
childToDelete.stateNode = instance;
childToDelete.return = returnFiber;
@@ -539,12 +545,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 +565,16 @@ function popHydrationState(fiber: Fiber): boolean {
return true;
}
+function hasMore() {
+ return isHydrating && nextHydratableInstance !== null;
+}
+
+function warnDeleteNextHydratableInstance(fiber: Fiber) {
+ if (nextHydratableInstance) {
+ warnDeleteHydratableInstance(fiber, nextHydratableInstance);
+ }
+}
+
function resetHydrationState(): void {
if (!supportsHydration) {
return;
@@ -581,4 +600,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 b7bca8217d979f..fb2db55b1b57ae 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
@@ -121,7 +121,7 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
return true;
}
-function deleteHydratableInstance(
+function warnDeleteHydratableInstance(
returnFiber: Fiber,
instance: HydratableInstance,
) {
@@ -151,7 +151,13 @@ function deleteHydratableInstance(
break;
}
}
+}
+function deleteHydratableInstance(
+ returnFiber: Fiber,
+ instance: HydratableInstance,
+) {
+ warnDeleteHydratableInstance(returnFiber, instance);
const childToDelete = createFiberFromHostInstanceForDeletion();
childToDelete.stateNode = instance;
childToDelete.return = returnFiber;
@@ -539,12 +545,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 +565,16 @@ function popHydrationState(fiber: Fiber): boolean {
return true;
}
+function hasMore() {
+ return isHydrating && nextHydratableInstance !== null;
+}
+
+function warnDeleteNextHydratableInstance(fiber: Fiber) {
+ if (nextHydratableInstance) {
+ warnDeleteHydratableInstance(fiber, nextHydratableInstance);
+ }
+}
+
function resetHydrationState(): void {
if (!supportsHydration) {
return;
@@ -581,4 +600,6 @@ export {
prepareToHydrateHostTextInstance,
prepareToHydrateHostSuspenseInstance,
popHydrationState,
+ hasMore,
+ warnDeleteNextHydratableInstance,
};