Skip to content

Commit 2af4a79

Browse files
sebmarkbagegaearon
andauthored
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>
1 parent b1acff0 commit 2af4a79

6 files changed

+293
-41
lines changed

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,64 @@ describe('ReactDOMServerPartialHydration', () => {
160160
expect(ref.current).toBe(span);
161161
});
162162

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+
163221
it('calls the hydration callbacks after hydration or deletion', async () => {
164222
let suspend = false;
165223
let resolve;

packages/react-dom/src/client/ReactDOMHostConfig.js

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -764,11 +764,23 @@ export function getNextHydratableSibling(
764764
}
765765

766766
export function getFirstHydratableChild(
767-
parentInstance: Container | Instance,
767+
parentInstance: Instance,
768768
): null | HydratableInstance {
769769
return getNextHydratable(parentInstance.firstChild);
770770
}
771771

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+
772784
export function hydrateInstance(
773785
instance: Instance,
774786
type: string,
@@ -917,7 +929,7 @@ export function didNotMatchHydratedTextInstance(
917929
}
918930
}
919931

920-
export function didNotHydrateContainerInstance(
932+
export function didNotHydrateInstanceWithinContainer(
921933
parentContainer: Container,
922934
instance: HydratableInstance,
923935
) {
@@ -932,6 +944,25 @@ export function didNotHydrateContainerInstance(
932944
}
933945
}
934946

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+
935966
export function didNotHydrateInstance(
936967
parentType: string,
937968
parentProps: Props,
@@ -949,7 +980,7 @@ export function didNotHydrateInstance(
949980
}
950981
}
951982

952-
export function didNotFindHydratableContainerInstance(
983+
export function didNotFindHydratableInstanceWithinContainer(
953984
parentContainer: Container,
954985
type: string,
955986
props: Props,
@@ -959,7 +990,7 @@ export function didNotFindHydratableContainerInstance(
959990
}
960991
}
961992

962-
export function didNotFindHydratableContainerTextInstance(
993+
export function didNotFindHydratableTextInstanceWithinContainer(
963994
parentContainer: Container,
964995
text: string,
965996
) {
@@ -968,14 +999,47 @@ export function didNotFindHydratableContainerTextInstance(
968999
}
9691000
}
9701001

971-
export function didNotFindHydratableContainerSuspenseInstance(
1002+
export function didNotFindHydratableSuspenseInstanceWithinContainer(
9721003
parentContainer: Container,
9731004
) {
9741005
if (__DEV__) {
9751006
// TODO: warnForInsertedHydratedSuspense(parentContainer);
9761007
}
9771008
}
9781009

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+
9791043
export function didNotFindHydratableInstance(
9801044
parentType: string,
9811045
parentProps: Props,

packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export const isSuspenseInstanceFallback = shim;
2929
export const registerSuspenseInstanceRetry = shim;
3030
export const getNextHydratableSibling = shim;
3131
export const getFirstHydratableChild = shim;
32+
export const getFirstHydratableChildWithinContainer = shim;
33+
export const getFirstHydratableChildWithinSuspenseInstance = shim;
3234
export const hydrateInstance = shim;
3335
export const hydrateTextInstance = shim;
3436
export const hydrateSuspenseInstance = shim;
@@ -40,11 +42,15 @@ export const clearSuspenseBoundaryFromContainer = shim;
4042
export const shouldDeleteUnhydratedTailInstances = shim;
4143
export const didNotMatchHydratedContainerTextInstance = shim;
4244
export const didNotMatchHydratedTextInstance = shim;
43-
export const didNotHydrateContainerInstance = shim;
45+
export const didNotHydrateInstanceWithinContainer = shim;
46+
export const didNotHydrateInstanceWithinSuspenseInstance = shim;
4447
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;
4854
export const didNotFindHydratableInstance = shim;
4955
export const didNotFindHydratableTextInstance = shim;
5056
export const didNotFindHydratableSuspenseInstance = shim;

packages/react-reconciler/src/ReactFiberHydrationContext.new.js

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,24 @@ import {
3838
canHydrateSuspenseInstance,
3939
getNextHydratableSibling,
4040
getFirstHydratableChild,
41+
getFirstHydratableChildWithinContainer,
42+
getFirstHydratableChildWithinSuspenseInstance,
4143
hydrateInstance,
4244
hydrateTextInstance,
4345
hydrateSuspenseInstance,
4446
getNextHydratableInstanceAfterSuspenseInstance,
4547
shouldDeleteUnhydratedTailInstances,
4648
didNotMatchHydratedContainerTextInstance,
4749
didNotMatchHydratedTextInstance,
48-
didNotHydrateContainerInstance,
50+
didNotHydrateInstanceWithinContainer,
51+
didNotHydrateInstanceWithinSuspenseInstance,
4952
didNotHydrateInstance,
50-
didNotFindHydratableContainerInstance,
51-
didNotFindHydratableContainerTextInstance,
52-
didNotFindHydratableContainerSuspenseInstance,
53+
didNotFindHydratableInstanceWithinContainer,
54+
didNotFindHydratableTextInstanceWithinContainer,
55+
didNotFindHydratableSuspenseInstanceWithinContainer,
56+
didNotFindHydratableInstanceWithinSuspenseInstance,
57+
didNotFindHydratableTextInstanceWithinSuspenseInstance,
58+
didNotFindHydratableSuspenseInstanceWithinSuspenseInstance,
5359
didNotFindHydratableInstance,
5460
didNotFindHydratableTextInstance,
5561
didNotFindHydratableSuspenseInstance,
@@ -78,8 +84,10 @@ function enterHydrationState(fiber: Fiber): boolean {
7884
return false;
7985
}
8086

81-
const parentInstance = fiber.stateNode.containerInfo;
82-
nextHydratableInstance = getFirstHydratableChild(parentInstance);
87+
const parentInstance: Container = fiber.stateNode.containerInfo;
88+
nextHydratableInstance = getFirstHydratableChildWithinContainer(
89+
parentInstance,
90+
);
8391
hydrationParentFiber = fiber;
8492
isHydrating = true;
8593
return true;
@@ -92,8 +100,10 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
92100
if (!supportsHydration) {
93101
return false;
94102
}
95-
nextHydratableInstance = getNextHydratableSibling(suspenseInstance);
96-
popToNextHostParent(fiber);
103+
nextHydratableInstance = getFirstHydratableChildWithinSuspenseInstance(
104+
suspenseInstance,
105+
);
106+
hydrationParentFiber = fiber;
97107
isHydrating = true;
98108
return true;
99109
}
@@ -105,7 +115,7 @@ function deleteHydratableInstance(
105115
if (__DEV__) {
106116
switch (returnFiber.tag) {
107117
case HostRoot:
108-
didNotHydrateContainerInstance(
118+
didNotHydrateInstanceWithinContainer(
109119
returnFiber.stateNode.containerInfo,
110120
instance,
111121
);
@@ -118,6 +128,14 @@ function deleteHydratableInstance(
118128
instance,
119129
);
120130
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;
121139
}
122140
}
123141

@@ -144,14 +162,23 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
144162
case HostComponent:
145163
const type = fiber.type;
146164
const props = fiber.pendingProps;
147-
didNotFindHydratableContainerInstance(parentContainer, type, props);
165+
didNotFindHydratableInstanceWithinContainer(
166+
parentContainer,
167+
type,
168+
props,
169+
);
148170
break;
149171
case HostText:
150172
const text = fiber.pendingProps;
151-
didNotFindHydratableContainerTextInstance(parentContainer, text);
173+
didNotFindHydratableTextInstanceWithinContainer(
174+
parentContainer,
175+
text,
176+
);
152177
break;
153178
case SuspenseComponent:
154-
didNotFindHydratableContainerSuspenseInstance(parentContainer);
179+
didNotFindHydratableSuspenseInstanceWithinContainer(
180+
parentContainer,
181+
);
155182
break;
156183
}
157184
break;
@@ -191,6 +218,35 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
191218
}
192219
break;
193220
}
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+
}
194250
default:
195251
return;
196252
}

0 commit comments

Comments
 (0)