Skip to content

Commit 6f3c833

Browse files
authored
Reset hydration state after reentering (#16306)
We might reenter a hydration state, when attempting to hydrate a boundary. We need to ensure that we reset it to not hydrating once we exit it. Otherwise the next sibling will still be in hydration mode.
1 parent 028c07f commit 6f3c833

File tree

5 files changed

+80
-11
lines changed

5 files changed

+80
-11
lines changed

packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,4 +1057,43 @@ describe('ReactDOMServerPartialHydration', () => {
10571057
expect(container.lastChild.nodeType).toBe(8);
10581058
expect(container.lastChild.data).toBe('unrelated comment');
10591059
});
1060+
1061+
it('can hydrate TWO suspense boundaries', async () => {
1062+
let ref1 = React.createRef();
1063+
let ref2 = React.createRef();
1064+
1065+
function App() {
1066+
return (
1067+
<div>
1068+
<Suspense fallback="Loading 1...">
1069+
<span ref={ref1}>1</span>
1070+
</Suspense>
1071+
<Suspense fallback="Loading 2...">
1072+
<span ref={ref2}>2</span>
1073+
</Suspense>
1074+
</div>
1075+
);
1076+
}
1077+
1078+
// First we render the final HTML. With the streaming renderer
1079+
// this may have suspense points on the server but here we want
1080+
// to test the completed HTML. Don't suspend on the server.
1081+
let finalHTML = ReactDOMServer.renderToString(<App />);
1082+
1083+
let container = document.createElement('div');
1084+
container.innerHTML = finalHTML;
1085+
1086+
let span1 = container.getElementsByTagName('span')[0];
1087+
let span2 = container.getElementsByTagName('span')[1];
1088+
1089+
// On the client we don't have all data yet but we want to start
1090+
// hydrating anyway.
1091+
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1092+
root.render(<App />);
1093+
Scheduler.unstable_flushAll();
1094+
jest.runAllTimers();
1095+
1096+
expect(ref1.current).toBe(span1);
1097+
expect(ref2.current).toBe(span2);
1098+
});
10601099
});

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ import {
148148
reenterHydrationStateFromDehydratedSuspenseInstance,
149149
resetHydrationState,
150150
tryToClaimNextHydratableInstance,
151+
warnIfHydrating,
151152
} from './ReactFiberHydrationContext';
152153
import {
153154
adoptClassInstance,
@@ -1910,12 +1911,18 @@ function updateDehydratedSuspenseComponent(
19101911
}
19111912
return null;
19121913
}
1914+
19131915
if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
19141916
// Something suspended. Leave the existing children in place.
19151917
// TODO: In non-concurrent mode, should we commit the nodes we have hydrated so far?
19161918
workInProgress.child = null;
19171919
return null;
19181920
}
1921+
1922+
// We should never be hydrating at this point because it is the first pass,
1923+
// but after we've already committed once.
1924+
warnIfHydrating();
1925+
19191926
if (isSuspenseInstanceFallback(suspenseInstance)) {
19201927
// This boundary is in a permanent fallback state. In this case, we'll never
19211928
// get an update and we'll never be able to hydrate the final content. Let's just try the

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ import {
117117
prepareToHydrateHostTextInstance,
118118
skipPastDehydratedSuspenseInstance,
119119
popHydrationState,
120+
resetHydrationState,
120121
} from './ReactFiberHydrationContext';
121122
import {
122123
enableSchedulerTracing,
@@ -982,15 +983,22 @@ function completeWork(
982983
markSpawnedWork(Never);
983984
}
984985
skipPastDehydratedSuspenseInstance(workInProgress);
985-
} else if ((workInProgress.effectTag & DidCapture) === NoEffect) {
986-
// This boundary did not suspend so it's now hydrated.
987-
// To handle any future suspense cases, we're going to now upgrade it
988-
// to a Suspense component. We detach it from the existing current fiber.
989-
current.alternate = null;
990-
workInProgress.alternate = null;
991-
workInProgress.tag = SuspenseComponent;
992-
workInProgress.memoizedState = null;
993-
workInProgress.stateNode = null;
986+
} else {
987+
// We should never have been in a hydration state if we didn't have a current.
988+
// However, in some of those paths, we might have reentered a hydration state
989+
// and then we might be inside a hydration state. In that case, we'll need to
990+
// exit out of it.
991+
resetHydrationState();
992+
if ((workInProgress.effectTag & DidCapture) === NoEffect) {
993+
// This boundary did not suspend so it's now hydrated.
994+
// To handle any future suspense cases, we're going to now upgrade it
995+
// to a Suspense component. We detach it from the existing current fiber.
996+
current.alternate = null;
997+
workInProgress.alternate = null;
998+
workInProgress.tag = SuspenseComponent;
999+
workInProgress.memoizedState = null;
1000+
workInProgress.stateNode = null;
1001+
}
9941002
}
9951003
}
9961004
break;

packages/react-reconciler/src/ReactFiberHydrationContext.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,23 @@ import {
5151
didNotFindHydratableSuspenseInstance,
5252
} from './ReactFiberHostConfig';
5353
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
54+
import warning from 'shared/warning';
5455

5556
// The deepest Fiber on the stack involved in a hydration context.
5657
// This may have been an insertion or a hydration.
5758
let hydrationParentFiber: null | Fiber = null;
5859
let nextHydratableInstance: null | HydratableInstance = null;
5960
let isHydrating: boolean = false;
6061

62+
function warnIfHydrating() {
63+
if (__DEV__) {
64+
warning(
65+
!isHydrating,
66+
'We should not be hydrating here. This is a bug in React. Please file a bug.',
67+
);
68+
}
69+
}
70+
6171
function enterHydrationState(fiber: Fiber): boolean {
6272
if (!supportsHydration) {
6373
return false;
@@ -432,6 +442,7 @@ function resetHydrationState(): void {
432442
}
433443

434444
export {
445+
warnIfHydrating,
435446
enterHydrationState,
436447
reenterHydrationStateFromDehydratedSuspenseInstance,
437448
resetHydrationState,

packages/react-reconciler/src/ReactFiberUnwindWork.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
2525

2626
import {popHostContainer, popHostContext} from './ReactFiberHostContext';
2727
import {popSuspenseContext} from './ReactFiberSuspenseContext';
28+
import {resetHydrationState} from './ReactFiberHydrationContext';
2829
import {
2930
isContextProvider as isLegacyContextProvider,
3031
popContext as popLegacyContext,
@@ -80,8 +81,12 @@ function unwindWork(
8081
}
8182
case DehydratedSuspenseComponent: {
8283
if (enableSuspenseServerRenderer) {
83-
// TODO: popHydrationState
8484
popSuspenseContext(workInProgress);
85+
if (workInProgress.alternate === null) {
86+
// TODO: popHydrationState
87+
} else {
88+
resetHydrationState();
89+
}
8590
const effectTag = workInProgress.effectTag;
8691
if (effectTag & ShouldCapture) {
8792
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
@@ -134,7 +139,6 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
134139
break;
135140
case DehydratedSuspenseComponent:
136141
if (enableSuspenseServerRenderer) {
137-
// TODO: popHydrationState
138142
popSuspenseContext(interruptedWork);
139143
}
140144
break;

0 commit comments

Comments
 (0)