@samitouri / QOS-React-2 / commits / 2af4a79333

Hydrate using SuspenseComponent as the parent (#22582)

* Add a failing test for Suspense hydration * Include salazarm's changes to the test * The hydration parent of a suspense boundary should be the boundary itself This eventually got set when we popped back out of its children but it doesn't start out that way. This fixes it so that the boundary parent is always the suspense boundary. * We now need to log errors with a suspense boundary as a parent For now, we just log this with commentNode.parentNode as the parent for purposes of the error message. * Make a special getFirstHydratableChildWithinSuspenseInstance We currently call getNextHydratableSibling but conceptually it's the child of the boundary. It just happens to be that when we use comment nodes, we need to call nextSibling in the DOM. This makes this separation a bit clearer. * Sync old fork Co-authored-by: Dan Abramov <dan.abramov@me.com>

Sebastian Markbåge committed Oct 18, 2021 at 20:44 UTC 2af4a79333108f59600970ef23e2614c3a5324e4
6 files changed +293 -41
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+58
@@ -160,6 +160,64 @@ describe('ReactDOMServerPartialHydration', () => {
160 expect(ref.current).toBe(span);
161 });
162
163 + it('can hydrate siblings of a suspended component without errors', async () => {
164 + let suspend = false;
165 + let resolve;
166 + const promise = new Promise(resolvePromise => (resolve = resolvePromise));
167 + function Child() {
168 + if (suspend) {
169 + throw promise;
170 + } else {
171 + return 'Hello';
172 + }
173 + }
174 +
175 + function App() {
176 + return (
177 + <Suspense fallback="Loading...">
178 + <Child />
179 + <Suspense fallback="Loading...">
180 + <div>Hello</div>
181 + </Suspense>
182 + </Suspense>
183 + );
184 + }
185 +
186 + // First we render the final HTML. With the streaming renderer
187 + // this may have suspense points on the server but here we want
188 + // to test the completed HTML. Don't suspend on the server.
189 + suspend = false;
190 + const finalHTML = ReactDOMServer.renderToString(<App />);
191 +
192 + const container = document.createElement('div');
193 + container.innerHTML = finalHTML;
194 + expect(container.textContent).toBe('HelloHello');
195 +
196 + // On the client we don't have all data yet but we want to start
197 + // hydrating anyway.
198 + suspend = true;
199 + ReactDOM.hydrateRoot(container, <App />);
200 + expect(() => {
201 + Scheduler.unstable_flushAll();
202 + }).toErrorDev(
203 + // TODO: This error should not be logged in this case. It's a false positive.
204 + 'Did not expect server HTML to contain the text node "Hello" in <div>.',
205 + );
206 + jest.runAllTimers();
207 +
208 + // Expect the server-generated HTML to stay intact.
209 + expect(container.textContent).toBe('HelloHello');
210 +
211 + // Resolving the promise should continue hydration
212 + suspend = false;
213 + resolve();
214 + await promise;
215 + Scheduler.unstable_flushAll();
216 + jest.runAllTimers();
217 + // Hydration should not change anything.
218 + expect(container.textContent).toBe('HelloHello');
219 + });
220 +
221 it('calls the hydration callbacks after hydration or deletion', async () => {
222 let suspend = false;
223 let resolve;
packages/react-dom/src/client/ReactDOMHostConfig.js
+69 -5
@@ -764,11 +764,23 @@ export function getNextHydratableSibling(
764 }
765
766 export function getFirstHydratableChild(
767 - parentInstance: Container | Instance,
767 + parentInstance: Instance,
768 ): null | HydratableInstance {
769 return getNextHydratable(parentInstance.firstChild);
770 }
771
772 +export function getFirstHydratableChildWithinContainer(
773 + parentContainer: Container,
774 +): null | HydratableInstance {
775 + return getNextHydratable(parentContainer.firstChild);
776 +}
777 +
778 +export function getFirstHydratableChildWithinSuspenseInstance(
779 + parentInstance: SuspenseInstance,
780 +): null | HydratableInstance {
781 + return getNextHydratable(parentInstance.nextSibling);
782 +}
783 +
784 export function hydrateInstance(
785 instance: Instance,
786 type: string,
@@ -917,7 +929,7 @@ export function didNotMatchHydratedTextInstance(
929 }
930 }
931
920 -export function didNotHydrateContainerInstance(
932 +export function didNotHydrateInstanceWithinContainer(
933 parentContainer: Container,
934 instance: HydratableInstance,
935 ) {
@@ -932,6 +944,25 @@ export function didNotHydrateContainerInstance(
944 }
945 }
946
947 +export function didNotHydrateInstanceWithinSuspenseInstance(
948 + parentInstance: SuspenseInstance,
949 + instance: HydratableInstance,
950 +) {
951 + if (__DEV__) {
952 + // $FlowFixMe: Only Element or Document can be parent nodes.
953 + const parentNode: Element | Document | null = parentInstance.parentNode;
954 + if (parentNode !== null) {
955 + if (instance.nodeType === ELEMENT_NODE) {
956 + warnForDeletedHydratableElement(parentNode, (instance: any));
957 + } else if (instance.nodeType === COMMENT_NODE) {
958 + // TODO: warnForDeletedHydratableSuspenseBoundary
959 + } else {
960 + warnForDeletedHydratableText(parentNode, (instance: any));
961 + }
962 + }
963 + }
964 +}
965 +
966 export function didNotHydrateInstance(
967 parentType: string,
968 parentProps: Props,
@@ -949,7 +980,7 @@ export function didNotHydrateInstance(
980 }
981 }
982
952 -export function didNotFindHydratableContainerInstance(
983 +export function didNotFindHydratableInstanceWithinContainer(
984 parentContainer: Container,
985 type: string,
986 props: Props,
@@ -959,7 +990,7 @@ export function didNotFindHydratableContainerInstance(
990 }
991 }
992
962 -export function didNotFindHydratableContainerTextInstance(
993 +export function didNotFindHydratableTextInstanceWithinContainer(
994 parentContainer: Container,
995 text: string,
996 ) {
@@ -968,7 +999,7 @@ export function didNotFindHydratableContainerTextInstance(
999 }
1000 }
1001
971 -export function didNotFindHydratableContainerSuspenseInstance(
1002 +export function didNotFindHydratableSuspenseInstanceWithinContainer(
1003 parentContainer: Container,
1004 ) {
1005 if (__DEV__) {
@@ -976,6 +1007,39 @@ export function didNotFindHydratableContainerSuspenseInstance(
1007 }
1008 }
1009
1010 +export function didNotFindHydratableInstanceWithinSuspenseInstance(
1011 + parentInstance: SuspenseInstance,
1012 + type: string,
1013 + props: Props,
1014 +) {
1015 + if (__DEV__) {
1016 + // $FlowFixMe: Only Element or Document can be parent nodes.
1017 + const parentNode: Element | Document | null = parentInstance.parentNode;
1018 + if (parentNode !== null)
1019 + warnForInsertedHydratedElement(parentNode, type, props);
1020 + }
1021 +}
1022 +
1023 +export function didNotFindHydratableTextInstanceWithinSuspenseInstance(
1024 + parentInstance: SuspenseInstance,
1025 + text: string,
1026 +) {
1027 + if (__DEV__) {
1028 + // $FlowFixMe: Only Element or Document can be parent nodes.
1029 + const parentNode: Element | Document | null = parentInstance.parentNode;
1030 + if (parentNode !== null) warnForInsertedHydratedText(parentNode, text);
1031 + }
1032 +}
1033 +
1034 +export function didNotFindHydratableSuspenseInstanceWithinSuspenseInstance(
1035 + parentInstance: SuspenseInstance,
1036 +) {
1037 + if (__DEV__) {
1038 + // const parentNode: Element | Document | null = parentInstance.parentNode;
1039 + // TODO: warnForInsertedHydratedSuspense(parentNode);
1040 + }
1041 +}
1042 +
1043 export function didNotFindHydratableInstance(
1044 parentType: string,
1045 parentProps: Props,
packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
+10 -4
@@ -29,6 +29,8 @@ export const isSuspenseInstanceFallback = shim;
29 export const registerSuspenseInstanceRetry = shim;
30 export const getNextHydratableSibling = shim;
31 export const getFirstHydratableChild = shim;
32 +export const getFirstHydratableChildWithinContainer = shim;
33 +export const getFirstHydratableChildWithinSuspenseInstance = shim;
34 export const hydrateInstance = shim;
35 export const hydrateTextInstance = shim;
36 export const hydrateSuspenseInstance = shim;
@@ -40,11 +42,15 @@ export const clearSuspenseBoundaryFromContainer = shim;
42 export const shouldDeleteUnhydratedTailInstances = shim;
43 export const didNotMatchHydratedContainerTextInstance = shim;
44 export const didNotMatchHydratedTextInstance = shim;
43 -export const didNotHydrateContainerInstance = shim;
45 +export const didNotHydrateInstanceWithinContainer = shim;
46 +export const didNotHydrateInstanceWithinSuspenseInstance = shim;
47 export const didNotHydrateInstance = shim;
45 -export const didNotFindHydratableContainerInstance = shim;
46 -export const didNotFindHydratableContainerTextInstance = shim;
47 -export const didNotFindHydratableContainerSuspenseInstance = shim;
48 +export const didNotFindHydratableInstanceWithinContainer = shim;
49 +export const didNotFindHydratableTextInstanceWithinContainer = shim;
50 +export const didNotFindHydratableSuspenseInstanceWithinContainer = shim;
51 +export const didNotFindHydratableInstanceWithinSuspenseInstance = shim;
52 +export const didNotFindHydratableTextInstanceWithinSuspenseInstance = shim;
53 +export const didNotFindHydratableSuspenseInstanceWithinSuspenseInstance = shim;
54 export const didNotFindHydratableInstance = shim;
55 export const didNotFindHydratableTextInstance = shim;
56 export const didNotFindHydratableSuspenseInstance = shim;
packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+68 -12
@@ -38,6 +38,8 @@ import {
38 canHydrateSuspenseInstance,
39 getNextHydratableSibling,
40 getFirstHydratableChild,
41 + getFirstHydratableChildWithinContainer,
42 + getFirstHydratableChildWithinSuspenseInstance,
43 hydrateInstance,
44 hydrateTextInstance,
45 hydrateSuspenseInstance,
@@ -45,11 +47,15 @@ import {
47 shouldDeleteUnhydratedTailInstances,
48 didNotMatchHydratedContainerTextInstance,
49 didNotMatchHydratedTextInstance,
48 - didNotHydrateContainerInstance,
50 + didNotHydrateInstanceWithinContainer,
51 + didNotHydrateInstanceWithinSuspenseInstance,
52 didNotHydrateInstance,
50 - didNotFindHydratableContainerInstance,
51 - didNotFindHydratableContainerTextInstance,
52 - didNotFindHydratableContainerSuspenseInstance,
53 + didNotFindHydratableInstanceWithinContainer,
54 + didNotFindHydratableTextInstanceWithinContainer,
55 + didNotFindHydratableSuspenseInstanceWithinContainer,
56 + didNotFindHydratableInstanceWithinSuspenseInstance,
57 + didNotFindHydratableTextInstanceWithinSuspenseInstance,
58 + didNotFindHydratableSuspenseInstanceWithinSuspenseInstance,
59 didNotFindHydratableInstance,
60 didNotFindHydratableTextInstance,
61 didNotFindHydratableSuspenseInstance,
@@ -78,8 +84,10 @@ function enterHydrationState(fiber: Fiber): boolean {
84 return false;
85 }
86
81 - const parentInstance = fiber.stateNode.containerInfo;
82 - nextHydratableInstance = getFirstHydratableChild(parentInstance);
87 + const parentInstance: Container = fiber.stateNode.containerInfo;
88 + nextHydratableInstance = getFirstHydratableChildWithinContainer(
89 + parentInstance,
90 + );
91 hydrationParentFiber = fiber;
92 isHydrating = true;
93 return true;
@@ -92,8 +100,10 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
100 if (!supportsHydration) {
101 return false;
102 }
95 - nextHydratableInstance = getNextHydratableSibling(suspenseInstance);
96 - popToNextHostParent(fiber);
103 + nextHydratableInstance = getFirstHydratableChildWithinSuspenseInstance(
104 + suspenseInstance,
105 + );
106 + hydrationParentFiber = fiber;
107 isHydrating = true;
108 return true;
109 }
@@ -105,7 +115,7 @@ function deleteHydratableInstance(
115 if (__DEV__) {
116 switch (returnFiber.tag) {
117 case HostRoot:
108 - didNotHydrateContainerInstance(
118 + didNotHydrateInstanceWithinContainer(
119 returnFiber.stateNode.containerInfo,
120 instance,
121 );
@@ -118,6 +128,14 @@ function deleteHydratableInstance(
128 instance,
129 );
130 break;
131 + case SuspenseComponent:
132 + const suspenseState: SuspenseState = returnFiber.memoizedState;
133 + if (suspenseState.dehydrated !== null)
134 + didNotHydrateInstanceWithinSuspenseInstance(
135 + suspenseState.dehydrated,
136 + instance,
137 + );
138 + break;
139 }
140 }
141
@@ -144,14 +162,23 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
162 case HostComponent:
163 const type = fiber.type;
164 const props = fiber.pendingProps;
147 - didNotFindHydratableContainerInstance(parentContainer, type, props);
165 + didNotFindHydratableInstanceWithinContainer(
166 + parentContainer,
167 + type,
168 + props,
169 + );
170 break;
171 case HostText:
172 const text = fiber.pendingProps;
151 - didNotFindHydratableContainerTextInstance(parentContainer, text);
173 + didNotFindHydratableTextInstanceWithinContainer(
174 + parentContainer,
175 + text,
176 + );
177 break;
178 case SuspenseComponent:
154 - didNotFindHydratableContainerSuspenseInstance(parentContainer);
179 + didNotFindHydratableSuspenseInstanceWithinContainer(
180 + parentContainer,
181 + );
182 break;
183 }
184 break;
@@ -191,6 +218,35 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
218 }
219 break;
220 }
221 + case SuspenseComponent: {
222 + const suspenseState: SuspenseState = returnFiber.memoizedState;
223 + const parentInstance = suspenseState.dehydrated;
224 + if (parentInstance !== null)
225 + switch (fiber.tag) {
226 + case HostComponent:
227 + const type = fiber.type;
228 + const props = fiber.pendingProps;
229 + didNotFindHydratableInstanceWithinSuspenseInstance(
230 + parentInstance,
231 + type,
232 + props,
233 + );
234 + break;
235 + case HostText:
236 + const text = fiber.pendingProps;
237 + didNotFindHydratableTextInstanceWithinSuspenseInstance(
238 + parentInstance,
239 + text,
240 + );
241 + break;
242 + case SuspenseComponent:
243 + didNotFindHydratableSuspenseInstanceWithinSuspenseInstance(
244 + parentInstance,
245 + );
246 + break;
247 + }
248 + break;
249 + }
250 default:
251 return;
252 }
packages/react-reconciler/src/ReactFiberHydrationContext.old.js
+68 -12
@@ -38,6 +38,8 @@ import {
38 canHydrateSuspenseInstance,
39 getNextHydratableSibling,
40 getFirstHydratableChild,
41 + getFirstHydratableChildWithinContainer,
42 + getFirstHydratableChildWithinSuspenseInstance,
43 hydrateInstance,
44 hydrateTextInstance,
45 hydrateSuspenseInstance,
@@ -45,11 +47,15 @@ import {
47 shouldDeleteUnhydratedTailInstances,
48 didNotMatchHydratedContainerTextInstance,
49 didNotMatchHydratedTextInstance,
48 - didNotHydrateContainerInstance,
50 + didNotHydrateInstanceWithinContainer,
51 + didNotHydrateInstanceWithinSuspenseInstance,
52 didNotHydrateInstance,
50 - didNotFindHydratableContainerInstance,
51 - didNotFindHydratableContainerTextInstance,
52 - didNotFindHydratableContainerSuspenseInstance,
53 + didNotFindHydratableInstanceWithinContainer,
54 + didNotFindHydratableTextInstanceWithinContainer,
55 + didNotFindHydratableSuspenseInstanceWithinContainer,
56 + didNotFindHydratableInstanceWithinSuspenseInstance,
57 + didNotFindHydratableTextInstanceWithinSuspenseInstance,
58 + didNotFindHydratableSuspenseInstanceWithinSuspenseInstance,
59 didNotFindHydratableInstance,
60 didNotFindHydratableTextInstance,
61 didNotFindHydratableSuspenseInstance,
@@ -78,8 +84,10 @@ function enterHydrationState(fiber: Fiber): boolean {
84 return false;
85 }
86
81 - const parentInstance = fiber.stateNode.containerInfo;
82 - nextHydratableInstance = getFirstHydratableChild(parentInstance);
87 + const parentInstance: Container = fiber.stateNode.containerInfo;
88 + nextHydratableInstance = getFirstHydratableChildWithinContainer(
89 + parentInstance,
90 + );
91 hydrationParentFiber = fiber;
92 isHydrating = true;
93 return true;
@@ -92,8 +100,10 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
100 if (!supportsHydration) {
101 return false;
102 }
95 - nextHydratableInstance = getNextHydratableSibling(suspenseInstance);
96 - popToNextHostParent(fiber);
103 + nextHydratableInstance = getFirstHydratableChildWithinSuspenseInstance(
104 + suspenseInstance,
105 + );
106 + hydrationParentFiber = fiber;
107 isHydrating = true;
108 return true;
109 }
@@ -105,7 +115,7 @@ function deleteHydratableInstance(
115 if (__DEV__) {
116 switch (returnFiber.tag) {
117 case HostRoot:
108 - didNotHydrateContainerInstance(
118 + didNotHydrateInstanceWithinContainer(
119 returnFiber.stateNode.containerInfo,
120 instance,
121 );
@@ -118,6 +128,14 @@ function deleteHydratableInstance(
128 instance,
129 );
130 break;
131 + case SuspenseComponent:
132 + const suspenseState: SuspenseState = returnFiber.memoizedState;
133 + if (suspenseState.dehydrated !== null)
134 + didNotHydrateInstanceWithinSuspenseInstance(
135 + suspenseState.dehydrated,
136 + instance,
137 + );
138 + break;
139 }
140 }
141
@@ -144,14 +162,23 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
162 case HostComponent:
163 const type = fiber.type;
164 const props = fiber.pendingProps;
147 - didNotFindHydratableContainerInstance(parentContainer, type, props);
165 + didNotFindHydratableInstanceWithinContainer(
166 + parentContainer,
167 + type,
168 + props,
169 + );
170 break;
171 case HostText:
172 const text = fiber.pendingProps;
151 - didNotFindHydratableContainerTextInstance(parentContainer, text);
173 + didNotFindHydratableTextInstanceWithinContainer(
174 + parentContainer,
175 + text,
176 + );
177 break;
178 case SuspenseComponent:
154 - didNotFindHydratableContainerSuspenseInstance(parentContainer);
179 + didNotFindHydratableSuspenseInstanceWithinContainer(
180 + parentContainer,
181 + );
182 break;
183 }
184 break;
@@ -191,6 +218,35 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
218 }
219 break;
220 }
221 + case SuspenseComponent: {
222 + const suspenseState: SuspenseState = returnFiber.memoizedState;
223 + const parentInstance = suspenseState.dehydrated;
224 + if (parentInstance !== null)
225 + switch (fiber.tag) {
226 + case HostComponent:
227 + const type = fiber.type;
228 + const props = fiber.pendingProps;
229 + didNotFindHydratableInstanceWithinSuspenseInstance(
230 + parentInstance,
231 + type,
232 + props,
233 + );
234 + break;
235 + case HostText:
236 + const text = fiber.pendingProps;
237 + didNotFindHydratableTextInstanceWithinSuspenseInstance(
238 + parentInstance,
239 + text,
240 + );
241 + break;
242 + case SuspenseComponent:
243 + didNotFindHydratableSuspenseInstanceWithinSuspenseInstance(
244 + parentInstance,
245 + );
246 + break;
247 + }
248 + break;
249 + }
250 default:
251 return;
252 }
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+20 -8
@@ -150,6 +150,10 @@ export const registerSuspenseInstanceRetry =
150 $$$hostConfig.registerSuspenseInstanceRetry;
151 export const getNextHydratableSibling = $$$hostConfig.getNextHydratableSibling;
152 export const getFirstHydratableChild = $$$hostConfig.getFirstHydratableChild;
153 +export const getFirstHydratableChildWithinContainer =
154 + $$$hostConfig.getFirstHydratableChildWithinContainer;
155 +export const getFirstHydratableChildWithinSuspenseInstance =
156 + $$$hostConfig.getFirstHydratableChildWithinSuspenseInstance;
157 export const hydrateInstance = $$$hostConfig.hydrateInstance;
158 export const hydrateTextInstance = $$$hostConfig.hydrateTextInstance;
159 export const hydrateSuspenseInstance = $$$hostConfig.hydrateSuspenseInstance;
@@ -167,15 +171,23 @@ export const didNotMatchHydratedContainerTextInstance =
171 $$$hostConfig.didNotMatchHydratedContainerTextInstance;
172 export const didNotMatchHydratedTextInstance =
173 $$$hostConfig.didNotMatchHydratedTextInstance;
170 -export const didNotHydrateContainerInstance =
171 - $$$hostConfig.didNotHydrateContainerInstance;
174 +export const didNotHydrateInstanceWithinContainer =
175 + $$$hostConfig.didNotHydrateInstanceWithinContainer;
176 +export const didNotHydrateInstanceWithinSuspenseInstance =
177 + $$$hostConfig.didNotHydrateInstanceWithinSuspenseInstance;
178 export const didNotHydrateInstance = $$$hostConfig.didNotHydrateInstance;
173 -export const didNotFindHydratableContainerInstance =
174 - $$$hostConfig.didNotFindHydratableContainerInstance;
175 -export const didNotFindHydratableContainerTextInstance =
176 - $$$hostConfig.didNotFindHydratableContainerTextInstance;
177 -export const didNotFindHydratableContainerSuspenseInstance =
178 - $$$hostConfig.didNotFindHydratableContainerSuspenseInstance;
179 +export const didNotFindHydratableInstanceWithinContainer =
180 + $$$hostConfig.didNotFindHydratableInstanceWithinContainer;
181 +export const didNotFindHydratableTextInstanceWithinContainer =
182 + $$$hostConfig.didNotFindHydratableTextInstanceWithinContainer;
183 +export const didNotFindHydratableSuspenseInstanceWithinContainer =
184 + $$$hostConfig.didNotFindHydratableSuspenseInstanceWithinContainer;
185 +export const didNotFindHydratableInstanceWithinSuspenseInstance =
186 + $$$hostConfig.didNotFindHydratableInstanceWithinSuspenseInstance;
187 +export const didNotFindHydratableTextInstanceWithinSuspenseInstance =
188 + $$$hostConfig.didNotFindHydratableTextInstanceWithinSuspenseInstance;
189 +export const didNotFindHydratableSuspenseInstanceWithinSuspenseInstance =
190 + $$$hostConfig.didNotFindHydratableSuspenseInstanceWithinSuspenseInstance;
191 export const didNotFindHydratableInstance =
192 $$$hostConfig.didNotFindHydratableInstance;
193 export const didNotFindHydratableTextInstance =