Skip to content

Commit 80efaa4

Browse files
committed
[DevTools] Handle dehydrated Suspense boundaries
1 parent ec57320 commit 80efaa4

File tree

1 file changed

+109
-80
lines changed
  • packages/react-devtools-shared/src/backend/fiber

1 file changed

+109
-80
lines changed

packages/react-devtools-shared/src/backend/fiber/renderer.js

Lines changed: 109 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3603,9 +3603,14 @@ export function attach(
36033603
} else {
36043604
const contentFiber = fiber.child;
36053605
if (contentFiber === null) {
3606-
throw new Error(
3607-
'There should always be an Offscreen Fiber child in a Suspense boundary.',
3608-
);
3606+
const suspenseState = fiber.memoizedState;
3607+
if (suspenseState === null || suspenseState.dehydrated === null) {
3608+
throw new Error(
3609+
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3610+
);
3611+
}
3612+
// This Suspense Fiber is still dehydrated. It won't have any children
3613+
// until hydration.
36093614
}
36103615
const isTimedOut = fiber.memoizedState !== null;
36113616
if (!isTimedOut) {
@@ -3654,12 +3659,17 @@ export function attach(
36543659
}
36553660
} else {
36563661
const contentFiber = fiber.child;
3662+
const suspenseState = fiber.memoizedState;
36573663
if (contentFiber === null) {
3658-
throw new Error(
3659-
'There should always be an Offscreen Fiber child in a Suspense boundary.',
3660-
);
3664+
if (suspenseState === null || suspenseState.dehydrated === null) {
3665+
throw new Error(
3666+
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3667+
);
3668+
}
3669+
// This Suspense Fiber is still dehydrated. It won't have any children
3670+
// until hydration.
36613671
}
3662-
const isTimedOut = fiber.memoizedState !== null;
3672+
const isTimedOut = suspenseState !== null;
36633673
if (!isTimedOut) {
36643674
newSuspenseNode.rects = measureInstance(newInstance);
36653675
}
@@ -3787,34 +3797,40 @@ export function attach(
37873797
// Modern Suspense path
37883798
const contentFiber = fiber.child;
37893799
if (contentFiber === null) {
3790-
throw new Error(
3791-
'There should always be an Offscreen Fiber child in a Suspense boundary.',
3792-
);
3793-
}
3794-
const fallbackFiber = contentFiber.sibling;
3795-
3796-
// First update only the Offscreen boundary. I.e. the main content.
3797-
mountVirtualChildrenRecursively(
3798-
contentFiber,
3799-
fallbackFiber,
3800-
traceNearestHostComponentUpdate,
3801-
0, // first level
3802-
);
3800+
const suspenseState = fiber.memoizedState;
3801+
if (suspenseState === null || suspenseState.dehydrated === null) {
3802+
throw new Error(
3803+
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3804+
);
3805+
}
3806+
// This Suspense Fiber is still dehydrated. It won't have any children
3807+
// until hydration.
3808+
} else {
3809+
const fallbackFiber = contentFiber.sibling;
38033810
3804-
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
3805-
// reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
3806-
// Since the fallback conceptually blocks the parent.
3807-
reconcilingParentSuspenseNode = stashedSuspenseParent;
3808-
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3809-
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3810-
shouldPopSuspenseNode = false;
3811-
if (fallbackFiber !== null) {
3811+
// First update only the Offscreen boundary. I.e. the main content.
38123812
mountVirtualChildrenRecursively(
3813+
contentFiber,
38133814
fallbackFiber,
3814-
null,
38153815
traceNearestHostComponentUpdate,
38163816
0, // first level
38173817
);
3818+
3819+
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
3820+
// reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
3821+
// Since the fallback conceptually blocks the parent.
3822+
reconcilingParentSuspenseNode = stashedSuspenseParent;
3823+
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3824+
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3825+
shouldPopSuspenseNode = false;
3826+
if (fallbackFiber !== null) {
3827+
mountVirtualChildrenRecursively(
3828+
fallbackFiber,
3829+
null,
3830+
traceNearestHostComponentUpdate,
3831+
0, // first level
3832+
);
3833+
}
38183834
}
38193835
} else {
38203836
if (fiber.child !== null) {
@@ -4719,61 +4735,74 @@ export function attach(
47194735
const prevContentFiber = prevFiber.child;
47204736
const nextContentFiber = nextFiber.child;
47214737
if (nextContentFiber === null || prevContentFiber === null) {
4722-
throw new Error(
4723-
'There should always be an Offscreen Fiber child in a Suspense boundary.',
4738+
const previousSuspenseState = prevFiber.memoizedState;
4739+
const nextSuspenseState = nextFiber.memoizedState;
4740+
if (
4741+
previousSuspenseState === null ||
4742+
previousSuspenseState.dehydrated === null ||
4743+
nextSuspenseState === null ||
4744+
nextSuspenseState.dehydrated === null
4745+
) {
4746+
throw new Error(
4747+
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
4748+
);
4749+
}
4750+
// This Suspense Fiber is still dehydrated. It won't have any children
4751+
// until hydration.
4752+
} else {
4753+
const prevFallbackFiber = prevContentFiber.sibling;
4754+
const nextFallbackFiber = nextContentFiber.sibling;
4755+
4756+
// First update only the Offscreen boundary. I.e. the main content.
4757+
updateFlags |= updateVirtualChildrenRecursively(
4758+
nextContentFiber,
4759+
nextFallbackFiber,
4760+
prevContentFiber,
4761+
traceNearestHostComponentUpdate,
4762+
0,
47244763
);
4725-
}
4726-
const prevFallbackFiber = prevContentFiber.sibling;
4727-
const nextFallbackFiber = nextContentFiber.sibling;
4728-
4729-
// First update only the Offscreen boundary. I.e. the main content.
4730-
updateFlags |= updateVirtualChildrenRecursively(
4731-
nextContentFiber,
4732-
nextFallbackFiber,
4733-
prevContentFiber,
4734-
traceNearestHostComponentUpdate,
4735-
0,
4736-
);
47374764
4738-
shouldMeasureSuspenseNode = false;
4739-
if (prevFallbackFiber !== null || nextFallbackFiber !== null) {
4740-
const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode;
4741-
const fallbackStashedSuspensePrevious =
4742-
previouslyReconciledSiblingSuspenseNode;
4743-
const fallbackStashedSuspenseRemaining =
4744-
remainingReconcilingChildrenSuspenseNodes;
4745-
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
4746-
// reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
4747-
// Since the fallback conceptually blocks the parent.
4748-
reconcilingParentSuspenseNode = stashedSuspenseParent;
4749-
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
4750-
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
4751-
try {
4752-
if (nextFallbackFiber === null) {
4753-
unmountRemainingChildren();
4754-
} else {
4755-
updateFlags |= updateVirtualChildrenRecursively(
4756-
nextFallbackFiber,
4757-
null,
4758-
prevFallbackFiber,
4759-
traceNearestHostComponentUpdate,
4760-
0,
4761-
);
4762-
}
4763-
} finally {
4764-
reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
4765-
previouslyReconciledSiblingSuspenseNode =
4766-
fallbackStashedSuspensePrevious;
4765+
shouldMeasureSuspenseNode = false;
4766+
if (prevFallbackFiber || nextFallbackFiber !== null) {
4767+
const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode;
4768+
const fallbackStashedSuspensePrevious =
4769+
previouslyReconciledSiblingSuspenseNode;
4770+
const fallbackStashedSuspenseRemaining =
4771+
remainingReconcilingChildrenSuspenseNodes;
4772+
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
4773+
// reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
4774+
// Since the fallback conceptually blocks the parent.
4775+
reconcilingParentSuspenseNode = stashedSuspenseParent;
4776+
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
47674777
remainingReconcilingChildrenSuspenseNodes =
4768-
fallbackStashedSuspenseRemaining;
4778+
stashedSuspenseRemaining;
4779+
try {
4780+
if (nextFallbackFiber === null) {
4781+
unmountRemainingChildren();
4782+
} else {
4783+
updateFlags |= updateVirtualChildrenRecursively(
4784+
nextFallbackFiber,
4785+
null,
4786+
prevFallbackFiber,
4787+
traceNearestHostComponentUpdate,
4788+
0,
4789+
);
4790+
}
4791+
} finally {
4792+
reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
4793+
previouslyReconciledSiblingSuspenseNode =
4794+
fallbackStashedSuspensePrevious;
4795+
remainingReconcilingChildrenSuspenseNodes =
4796+
fallbackStashedSuspenseRemaining;
4797+
}
4798+
}
4799+
if (nextFiber.memoizedState === null) {
4800+
// Measure this Suspense node in case it changed. We don't update the rect while
4801+
// we're inside a disconnected subtree nor if we are the Suspense boundary that
4802+
// is suspended. This lets us keep the rectangle of the displayed content while
4803+
// we're suspended to visualize the resulting state.
4804+
shouldMeasureSuspenseNode = !isInDisconnectedSubtree;
47694805
}
4770-
}
4771-
if (nextFiber.memoizedState === null) {
4772-
// Measure this Suspense node in case it changed. We don't update the rect while
4773-
// we're inside a disconnected subtree nor if we are the Suspense boundary that
4774-
// is suspended. This lets us keep the rectangle of the displayed content while
4775-
// we're suspended to visualize the resulting state.
4776-
shouldMeasureSuspenseNode = !isInDisconnectedSubtree;
47774806
}
47784807
} else {
47794808
// Common case: Primary -> Primary.

0 commit comments

Comments
 (0)