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.
Sebastian Markbåge committed
Aug 7, 2019 at 14:56 UTC
6f3c8332d8b2f92784a731e6cc6a707a92495a23
5 files changed
+80
-11
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+39
@@ -1057,4 +1057,43 @@ describe('ReactDOMServerPartialHydration', () => {
1057
expect(container.lastChild.nodeType).toBe(8);
1058
expect(container.lastChild.data).toBe('unrelated comment');
1059
});
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
+ });
1099
});
packages/react-reconciler/src/ReactFiberBeginWork.js
+7
@@ -148,6 +148,7 @@ import {
148
reenterHydrationStateFromDehydratedSuspenseInstance,
149
resetHydrationState,
150
tryToClaimNextHydratableInstance,
151
+ warnIfHydrating,
152
} from './ReactFiberHydrationContext';
153
import {
154
adoptClassInstance,
@@ -1910,12 +1911,18 @@ function updateDehydratedSuspenseComponent(
1911
}
1912
return null;
1913
}
1914
+
1915
if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
1916
// Something suspended. Leave the existing children in place.
1917
// TODO: In non-concurrent mode, should we commit the nodes we have hydrated so far?
1918
workInProgress.child = null;
1919
return null;
1920
}
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
+
1926
if (isSuspenseInstanceFallback(suspenseInstance)) {
1927
// This boundary is in a permanent fallback state. In this case, we'll never
1928
// 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
+17
-9
@@ -117,6 +117,7 @@ import {
117
prepareToHydrateHostTextInstance,
118
skipPastDehydratedSuspenseInstance,
119
popHydrationState,
120
+ resetHydrationState,
121
} from './ReactFiberHydrationContext';
122
import {
123
enableSchedulerTracing,
@@ -982,15 +983,22 @@ function completeWork(
983
markSpawnedWork(Never);
984
}
985
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
+ }
1002
}
1003
}
1004
break;
packages/react-reconciler/src/ReactFiberHydrationContext.js
+11
@@ -51,6 +51,7 @@ import {
51
didNotFindHydratableSuspenseInstance,
52
} from './ReactFiberHostConfig';
53
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
54
+import warning from 'shared/warning';
55
56
// The deepest Fiber on the stack involved in a hydration context.
57
// This may have been an insertion or a hydration.
@@ -58,6 +59,15 @@ let hydrationParentFiber: null | Fiber = null;
59
let nextHydratableInstance: null | HydratableInstance = null;
60
let isHydrating: boolean = false;
61
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
+
71
function enterHydrationState(fiber: Fiber): boolean {
72
if (!supportsHydration) {
73
return false;
@@ -432,6 +442,7 @@ function resetHydrationState(): void {
442
}
443
444
export {
445
+ warnIfHydrating,
446
enterHydrationState,
447
reenterHydrationStateFromDehydratedSuspenseInstance,
448
resetHydrationState,
packages/react-reconciler/src/ReactFiberUnwindWork.js
+6
-2
@@ -25,6 +25,7 @@ import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
25
26
import {popHostContainer, popHostContext} from './ReactFiberHostContext';
27
import {popSuspenseContext} from './ReactFiberSuspenseContext';
28
+import {resetHydrationState} from './ReactFiberHydrationContext';
29
import {
30
isContextProvider as isLegacyContextProvider,
31
popContext as popLegacyContext,
@@ -80,8 +81,12 @@ function unwindWork(
81
}
82
case DehydratedSuspenseComponent: {
83
if (enableSuspenseServerRenderer) {
83
- // TODO: popHydrationState
84
popSuspenseContext(workInProgress);
85
+ if (workInProgress.alternate === null) {
86
+ // TODO: popHydrationState
87
+ } else {
88
+ resetHydrationState();
89
+ }
90
const effectTag = workInProgress.effectTag;
91
if (effectTag & ShouldCapture) {
92
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
@@ -134,7 +139,6 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
139
break;
140
case DehydratedSuspenseComponent:
141
if (enableSuspenseServerRenderer) {
137
- // TODO: popHydrationState
142
popSuspenseContext(interruptedWork);
143
}
144
break;