@samitouri / QOS-React-2 / commits / 6f3c8332d8

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;