.',
- {withoutStack: true},
);
// The content should've been client rendered and replaced the
// existing div.
expect(ref.current).not.toBe(div);
// The HTML should be the same though.
- expect(element.innerHTML).toBe('
Hello World
');
+ expect(element.innerHTML).toBe('
');
+ });
+
+ it('Suspense + hydration in legacy mode (at root)', () => {
+ const element = document.createElement('div');
+ element.innerHTML = '
Hello World
';
+ const div = element.firstChild;
+ const ref = React.createRef();
+ ReactDOM.hydrate(
+
+ Hello World
+ ,
+ element,
+ );
+
+ // The content should've been client rendered.
+ expect(ref.current).not.toBe(div);
+ // Unfortunately, since we don't delete the tail at the root, a duplicate will remain.
+ expect(element.innerHTML).toBe(
+ '
Hello World
Hello World
',
+ );
});
it('Suspense + hydration in legacy mode with no fallback', () => {
diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js
index dfd29b375a41b..156951ac748bc 100644
--- a/packages/react-dom/src/client/ReactDOMHostConfig.js
+++ b/packages/react-dom/src/client/ReactDOMHostConfig.js
@@ -894,6 +894,12 @@ export function commitHydratedSuspenseInstance(
retryIfBlockedOn(suspenseInstance);
}
+export function shouldDeleteUnhydratedTailInstances(
+ parentType: string,
+): boolean {
+ return parentType !== 'head' || parentType !== 'body';
+}
+
export function didNotMatchHydratedContainerTextInstance(
parentContainer: Container,
textInstance: TextInstance,
@@ -1008,6 +1014,15 @@ export function didNotFindHydratableSuspenseInstance(
}
}
+export function errorHydratingContainer(parentContainer: Container): void {
+ if (__DEV__) {
+ console.error(
+ 'An error occurred during hydration. The server HTML was replaced with client content in <%s>.',
+ parentContainer.nodeName.toLowerCase(),
+ );
+ }
+}
+
export function getInstanceFromNode(node: HTMLElement): null | Object {
return getClosestInstanceFromNode(node) || null;
}
diff --git a/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js b/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
index b432a1d400736..c57b313fd90fa 100644
--- a/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
+++ b/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
@@ -40,6 +40,7 @@ export const commitHydratedContainer = shim;
export const commitHydratedSuspenseInstance = shim;
export const clearSuspenseBoundary = shim;
export const clearSuspenseBoundaryFromContainer = shim;
+export const shouldDeleteUnhydratedTailInstances = shim;
export const didNotMatchHydratedContainerTextInstance = shim;
export const didNotMatchHydratedTextInstance = shim;
export const didNotHydrateContainerInstance = shim;
@@ -50,3 +51,4 @@ export const didNotFindHydratableContainerSuspenseInstance = shim;
export const didNotFindHydratableInstance = shim;
export const didNotFindHydratableTextInstance = shim;
export const didNotFindHydratableSuspenseInstance = shim;
+export const errorHydratingContainer = shim;
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
index d91901f519764..2ebb1386a5684 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
@@ -43,6 +43,7 @@ import {
hydrateTextInstance,
hydrateSuspenseInstance,
getNextHydratableInstanceAfterSuspenseInstance,
+ shouldDeleteUnhydratedTailInstances,
didNotMatchHydratedContainerTextInstance,
didNotMatchHydratedTextInstance,
didNotHydrateContainerInstance,
@@ -438,18 +439,15 @@ function popHydrationState(fiber: Fiber): boolean {
return false;
}
- const type = fiber.type;
-
// If we have any remaining hydratable nodes, we need to delete them now.
// We only do this deeper than head and body since they tend to have random
// other nodes in them. We also ignore components with pure text content in
- // side of them.
- // TODO: Better heuristic.
+ // side of them. We also don't delete anything inside the root container.
if (
- fiber.tag !== HostComponent ||
- (type !== 'head' &&
- type !== 'body' &&
- !shouldSetTextContent(type, fiber.memoizedProps))
+ fiber.tag !== HostRoot &&
+ (fiber.tag !== HostComponent ||
+ (shouldDeleteUnhydratedTailInstances(fiber.type) &&
+ !shouldSetTextContent(fiber.type, fiber.memoizedProps)))
) {
let nextInstance = nextHydratableInstance;
while (nextInstance) {
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
index 6f7c51e0a569f..c8e17e0b6374f 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
@@ -43,6 +43,7 @@ import {
hydrateTextInstance,
hydrateSuspenseInstance,
getNextHydratableInstanceAfterSuspenseInstance,
+ shouldDeleteUnhydratedTailInstances,
didNotMatchHydratedContainerTextInstance,
didNotMatchHydratedTextInstance,
didNotHydrateContainerInstance,
@@ -438,18 +439,15 @@ function popHydrationState(fiber: Fiber): boolean {
return false;
}
- const type = fiber.type;
-
// If we have any remaining hydratable nodes, we need to delete them now.
// We only do this deeper than head and body since they tend to have random
// other nodes in them. We also ignore components with pure text content in
- // side of them.
- // TODO: Better heuristic.
+ // side of them. We also don't delete anything inside the root container.
if (
- fiber.tag !== HostComponent ||
- (type !== 'head' &&
- type !== 'body' &&
- !shouldSetTextContent(type, fiber.memoizedProps))
+ fiber.tag !== HostRoot &&
+ (fiber.tag !== HostComponent ||
+ (shouldDeleteUnhydratedTailInstances(fiber.type) &&
+ !shouldSetTextContent(fiber.type, fiber.memoizedProps)))
) {
let nextInstance = nextHydratableInstance;
while (nextInstance) {
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
index 19d53553f43d8..ea425725250ed 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
@@ -88,6 +88,7 @@ import {
clearContainer,
getCurrentEventPriority,
supportsMicrotasks,
+ errorHydratingContainer,
} from './ReactFiberHostConfig';
import {
@@ -782,6 +783,9 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
// discard server response and fall back to client side render.
if (root.hydrate) {
root.hydrate = false;
+ if (__DEV__) {
+ errorHydratingContainer(root.containerInfo);
+ }
clearContainer(root.containerInfo);
}
@@ -992,6 +996,9 @@ function performSyncWorkOnRoot(root) {
// discard server response and fall back to client side render.
if (root.hydrate) {
root.hydrate = false;
+ if (__DEV__) {
+ errorHydratingContainer(root.containerInfo);
+ }
clearContainer(root.containerInfo);
}
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
index da22638e75758..c29c9353b1732 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
@@ -88,6 +88,7 @@ import {
clearContainer,
getCurrentEventPriority,
supportsMicrotasks,
+ errorHydratingContainer,
} from './ReactFiberHostConfig';
import {
@@ -782,6 +783,9 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
// discard server response and fall back to client side render.
if (root.hydrate) {
root.hydrate = false;
+ if (__DEV__) {
+ errorHydratingContainer(root.containerInfo);
+ }
clearContainer(root.containerInfo);
}
@@ -992,6 +996,9 @@ function performSyncWorkOnRoot(root) {
// discard server response and fall back to client side render.
if (root.hydrate) {
root.hydrate = false;
+ if (__DEV__) {
+ errorHydratingContainer(root.containerInfo);
+ }
clearContainer(root.containerInfo);
}
diff --git a/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js b/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js
index c322a0a48ed4c..f737994ad7e70 100644
--- a/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js
+++ b/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js
@@ -214,7 +214,8 @@ describe('useMutableSourceHydration', () => {
source.value = 'two';
});
}).toErrorDev(
- 'Warning: Did not expect server HTML to contain a
in
.',
+ 'Warning: An error occurred during hydration. ' +
+ 'The server HTML was replaced with client content in
.',
{withoutStack: true},
);
expect(Scheduler).toHaveYielded(['only:two']);
@@ -266,7 +267,8 @@ describe('useMutableSourceHydration', () => {
source.value = 'two';
});
}).toErrorDev(
- 'Warning: Did not expect server HTML to contain a
in
.',
+ 'Warning: An error occurred during hydration. ' +
+ 'The server HTML was replaced with client content in
.',
{withoutStack: true},
);
expect(Scheduler).toHaveYielded(['a:two', 'b:two']);
@@ -334,7 +336,8 @@ describe('useMutableSourceHydration', () => {
source.valueB = 'b:two';
});
}).toErrorDev(
- 'Warning: Did not expect server HTML to contain a
in
.',
+ 'Warning: An error occurred during hydration. ' +
+ 'The server HTML was replaced with client content in
.',
{withoutStack: true},
);
expect(Scheduler).toHaveYielded(['0:a:one', '1:b:two']);
@@ -401,7 +404,13 @@ describe('useMutableSourceHydration', () => {
source.value = 'two';
});
}).toErrorDev(
- 'Warning: Text content did not match. Server: "1" Client: "2"',
+ [
+ 'Warning: An error occurred during hydration. ' +
+ 'The server HTML was replaced with client content in
.',
+
+ 'Warning: Text content did not match. Server: "1" Client: "2"',
+ ],
+ {withoutStack: 1},
);
expect(Scheduler).toHaveYielded([2, 'a:two']);
expect(source.listenerCount).toBe(1);
diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
index a2f065102d05f..5bfdf3a305f32 100644
--- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
@@ -156,6 +156,8 @@ export const commitHydratedSuspenseInstance =
export const clearSuspenseBoundary = $$$hostConfig.clearSuspenseBoundary;
export const clearSuspenseBoundaryFromContainer =
$$$hostConfig.clearSuspenseBoundaryFromContainer;
+export const shouldDeleteUnhydratedTailInstances =
+ $$$hostConfig.shouldDeleteUnhydratedTailInstances;
export const didNotMatchHydratedContainerTextInstance =
$$$hostConfig.didNotMatchHydratedContainerTextInstance;
export const didNotMatchHydratedTextInstance =
@@ -175,3 +177,4 @@ export const didNotFindHydratableTextInstance =
$$$hostConfig.didNotFindHydratableTextInstance;
export const didNotFindHydratableSuspenseInstance =
$$$hostConfig.didNotFindHydratableSuspenseInstance;
+export const errorHydratingContainer = $$$hostConfig.errorHydratingContainer;