Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hydrate using SuspenseComponent as the parent #22582

Merged
merged 6 commits into from
Oct 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,64 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current).toBe(span);
});

it('can hydrate siblings of a suspended component without errors', async () => {
let suspend = false;
let resolve;
const promise = new Promise(resolvePromise => (resolve = resolvePromise));
function Child() {
if (suspend) {
throw promise;
} else {
return 'Hello';
}
}

function App() {
return (
<Suspense fallback="Loading...">
<Child />
<Suspense fallback="Loading...">
<div>Hello</div>
</Suspense>
</Suspense>
);
}

// First we render the final HTML. With the streaming renderer
// this may have suspense points on the server but here we want
// to test the completed HTML. Don't suspend on the server.
suspend = false;
const finalHTML = ReactDOMServer.renderToString(<App />);

const container = document.createElement('div');
container.innerHTML = finalHTML;
expect(container.textContent).toBe('HelloHello');

// On the client we don't have all data yet but we want to start
// hydrating anyway.
suspend = true;
ReactDOM.hydrateRoot(container, <App />);
expect(() => {
Scheduler.unstable_flushAll();
}).toErrorDev(
// TODO: This error should not be logged in this case. It's a false positive.
'Did not expect server HTML to contain the text node "Hello" in <div>.',
);
jest.runAllTimers();

// Expect the server-generated HTML to stay intact.
expect(container.textContent).toBe('HelloHello');

// Resolving the promise should continue hydration
suspend = false;
resolve();
await promise;
Scheduler.unstable_flushAll();
jest.runAllTimers();
// Hydration should not change anything.
expect(container.textContent).toBe('HelloHello');
});

it('calls the hydration callbacks after hydration or deletion', async () => {
let suspend = false;
let resolve;
Expand Down
74 changes: 69 additions & 5 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -764,11 +764,23 @@ export function getNextHydratableSibling(
}

export function getFirstHydratableChild(
parentInstance: Container | Instance,
parentInstance: Instance,
): null | HydratableInstance {
return getNextHydratable(parentInstance.firstChild);
}

export function getFirstHydratableChildWithinContainer(
parentContainer: Container,
): null | HydratableInstance {
return getNextHydratable(parentContainer.firstChild);
}

export function getFirstHydratableChildWithinSuspenseInstance(
parentInstance: SuspenseInstance,
): null | HydratableInstance {
return getNextHydratable(parentInstance.nextSibling);
}

export function hydrateInstance(
instance: Instance,
type: string,
Expand Down Expand Up @@ -917,7 +929,7 @@ export function didNotMatchHydratedTextInstance(
}
}

export function didNotHydrateContainerInstance(
export function didNotHydrateInstanceWithinContainer(
parentContainer: Container,
instance: HydratableInstance,
) {
Expand All @@ -932,6 +944,25 @@ export function didNotHydrateContainerInstance(
}
}

export function didNotHydrateInstanceWithinSuspenseInstance(
parentInstance: SuspenseInstance,
instance: HydratableInstance,
) {
if (__DEV__) {
// $FlowFixMe: Only Element or Document can be parent nodes.
const parentNode: Element | Document | null = parentInstance.parentNode;
if (parentNode !== null) {
if (instance.nodeType === ELEMENT_NODE) {
warnForDeletedHydratableElement(parentNode, (instance: any));
} else if (instance.nodeType === COMMENT_NODE) {
// TODO: warnForDeletedHydratableSuspenseBoundary
} else {
warnForDeletedHydratableText(parentNode, (instance: any));
}
}
}
}

export function didNotHydrateInstance(
parentType: string,
parentProps: Props,
Expand All @@ -949,7 +980,7 @@ export function didNotHydrateInstance(
}
}

export function didNotFindHydratableContainerInstance(
export function didNotFindHydratableInstanceWithinContainer(
parentContainer: Container,
type: string,
props: Props,
Expand All @@ -959,7 +990,7 @@ export function didNotFindHydratableContainerInstance(
}
}

export function didNotFindHydratableContainerTextInstance(
export function didNotFindHydratableTextInstanceWithinContainer(
parentContainer: Container,
text: string,
) {
Expand All @@ -968,14 +999,47 @@ export function didNotFindHydratableContainerTextInstance(
}
}

export function didNotFindHydratableContainerSuspenseInstance(
export function didNotFindHydratableSuspenseInstanceWithinContainer(
parentContainer: Container,
) {
if (__DEV__) {
// TODO: warnForInsertedHydratedSuspense(parentContainer);
}
}

export function didNotFindHydratableInstanceWithinSuspenseInstance(
parentInstance: SuspenseInstance,
type: string,
props: Props,
) {
if (__DEV__) {
// $FlowFixMe: Only Element or Document can be parent nodes.
const parentNode: Element | Document | null = parentInstance.parentNode;
if (parentNode !== null)
warnForInsertedHydratedElement(parentNode, type, props);
}
}

export function didNotFindHydratableTextInstanceWithinSuspenseInstance(
parentInstance: SuspenseInstance,
text: string,
) {
if (__DEV__) {
// $FlowFixMe: Only Element or Document can be parent nodes.
const parentNode: Element | Document | null = parentInstance.parentNode;
if (parentNode !== null) warnForInsertedHydratedText(parentNode, text);
}
}

export function didNotFindHydratableSuspenseInstanceWithinSuspenseInstance(
parentInstance: SuspenseInstance,
) {
if (__DEV__) {
// const parentNode: Element | Document | null = parentInstance.parentNode;
// TODO: warnForInsertedHydratedSuspense(parentNode);
}
}

export function didNotFindHydratableInstance(
parentType: string,
parentProps: Props,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export const isSuspenseInstanceFallback = shim;
export const registerSuspenseInstanceRetry = shim;
export const getNextHydratableSibling = shim;
export const getFirstHydratableChild = shim;
export const getFirstHydratableChildWithinContainer = shim;
export const getFirstHydratableChildWithinSuspenseInstance = shim;
export const hydrateInstance = shim;
export const hydrateTextInstance = shim;
export const hydrateSuspenseInstance = shim;
Expand All @@ -40,11 +42,15 @@ export const clearSuspenseBoundaryFromContainer = shim;
export const shouldDeleteUnhydratedTailInstances = shim;
export const didNotMatchHydratedContainerTextInstance = shim;
export const didNotMatchHydratedTextInstance = shim;
export const didNotHydrateContainerInstance = shim;
export const didNotHydrateInstanceWithinContainer = shim;
export const didNotHydrateInstanceWithinSuspenseInstance = shim;
export const didNotHydrateInstance = shim;
export const didNotFindHydratableContainerInstance = shim;
export const didNotFindHydratableContainerTextInstance = shim;
export const didNotFindHydratableContainerSuspenseInstance = shim;
export const didNotFindHydratableInstanceWithinContainer = shim;
export const didNotFindHydratableTextInstanceWithinContainer = shim;
export const didNotFindHydratableSuspenseInstanceWithinContainer = shim;
export const didNotFindHydratableInstanceWithinSuspenseInstance = shim;
export const didNotFindHydratableTextInstanceWithinSuspenseInstance = shim;
export const didNotFindHydratableSuspenseInstanceWithinSuspenseInstance = shim;
export const didNotFindHydratableInstance = shim;
export const didNotFindHydratableTextInstance = shim;
export const didNotFindHydratableSuspenseInstance = shim;
Expand Down
80 changes: 68 additions & 12 deletions packages/react-reconciler/src/ReactFiberHydrationContext.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,24 @@ import {
canHydrateSuspenseInstance,
getNextHydratableSibling,
getFirstHydratableChild,
getFirstHydratableChildWithinContainer,
getFirstHydratableChildWithinSuspenseInstance,
hydrateInstance,
hydrateTextInstance,
hydrateSuspenseInstance,
getNextHydratableInstanceAfterSuspenseInstance,
shouldDeleteUnhydratedTailInstances,
didNotMatchHydratedContainerTextInstance,
didNotMatchHydratedTextInstance,
didNotHydrateContainerInstance,
didNotHydrateInstanceWithinContainer,
didNotHydrateInstanceWithinSuspenseInstance,
didNotHydrateInstance,
didNotFindHydratableContainerInstance,
didNotFindHydratableContainerTextInstance,
didNotFindHydratableContainerSuspenseInstance,
didNotFindHydratableInstanceWithinContainer,
didNotFindHydratableTextInstanceWithinContainer,
didNotFindHydratableSuspenseInstanceWithinContainer,
didNotFindHydratableInstanceWithinSuspenseInstance,
didNotFindHydratableTextInstanceWithinSuspenseInstance,
didNotFindHydratableSuspenseInstanceWithinSuspenseInstance,
didNotFindHydratableInstance,
didNotFindHydratableTextInstance,
didNotFindHydratableSuspenseInstance,
Expand Down Expand Up @@ -78,8 +84,10 @@ function enterHydrationState(fiber: Fiber): boolean {
return false;
}

const parentInstance = fiber.stateNode.containerInfo;
nextHydratableInstance = getFirstHydratableChild(parentInstance);
const parentInstance: Container = fiber.stateNode.containerInfo;
nextHydratableInstance = getFirstHydratableChildWithinContainer(
parentInstance,
);
hydrationParentFiber = fiber;
isHydrating = true;
return true;
Expand All @@ -92,8 +100,10 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
if (!supportsHydration) {
return false;
}
nextHydratableInstance = getNextHydratableSibling(suspenseInstance);
popToNextHostParent(fiber);
nextHydratableInstance = getFirstHydratableChildWithinSuspenseInstance(
suspenseInstance,
);
hydrationParentFiber = fiber;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main change.

isHydrating = true;
return true;
}
Expand All @@ -105,7 +115,7 @@ function deleteHydratableInstance(
if (__DEV__) {
switch (returnFiber.tag) {
case HostRoot:
didNotHydrateContainerInstance(
didNotHydrateInstanceWithinContainer(
returnFiber.stateNode.containerInfo,
instance,
);
Expand All @@ -118,6 +128,14 @@ function deleteHydratableInstance(
instance,
);
break;
case SuspenseComponent:
const suspenseState: SuspenseState = returnFiber.memoizedState;
if (suspenseState.dehydrated !== null)
didNotHydrateInstanceWithinSuspenseInstance(
suspenseState.dehydrated,
instance,
);
break;
}
}

Expand All @@ -144,14 +162,23 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
case HostComponent:
const type = fiber.type;
const props = fiber.pendingProps;
didNotFindHydratableContainerInstance(parentContainer, type, props);
didNotFindHydratableInstanceWithinContainer(
parentContainer,
type,
props,
);
break;
case HostText:
const text = fiber.pendingProps;
didNotFindHydratableContainerTextInstance(parentContainer, text);
didNotFindHydratableTextInstanceWithinContainer(
parentContainer,
text,
);
break;
case SuspenseComponent:
didNotFindHydratableContainerSuspenseInstance(parentContainer);
didNotFindHydratableSuspenseInstanceWithinContainer(
parentContainer,
);
break;
}
break;
Expand Down Expand Up @@ -191,6 +218,35 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
}
break;
}
case SuspenseComponent: {
const suspenseState: SuspenseState = returnFiber.memoizedState;
const parentInstance = suspenseState.dehydrated;
if (parentInstance !== null)
switch (fiber.tag) {
case HostComponent:
const type = fiber.type;
const props = fiber.pendingProps;
didNotFindHydratableInstanceWithinSuspenseInstance(
parentInstance,
type,
props,
);
break;
case HostText:
const text = fiber.pendingProps;
didNotFindHydratableTextInstanceWithinSuspenseInstance(
parentInstance,
text,
);
break;
case SuspenseComponent:
didNotFindHydratableSuspenseInstanceWithinSuspenseInstance(
parentInstance,
);
break;
}
break;
}
default:
return;
}
Expand Down
Loading