Skip to content

Commit 3d11e62

Browse files
committed
[DevTools] Handle dehydrated Suspense boundaries
1 parent 431bb0b commit 3d11e62

File tree

1 file changed

+119
-88
lines changed
  • packages/react-devtools-shared/src/backend/fiber

1 file changed

+119
-88
lines changed

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

Lines changed: 119 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -3634,9 +3634,14 @@ export function attach(
36343634
} else {
36353635
const contentFiber = fiber.child;
36363636
if (contentFiber === null) {
3637-
throw new Error(
3638-
'There should always be an Offscreen Fiber child in a Suspense boundary.',
3639-
);
3637+
const suspenseState = fiber.memoizedState;
3638+
if (suspenseState === null || suspenseState.dehydrated === null) {
3639+
throw new Error(
3640+
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3641+
);
3642+
}
3643+
// This Suspense Fiber is still dehydrated. It won't have any children
3644+
// until hydration.
36403645
}
36413646
const isTimedOut = fiber.memoizedState !== null;
36423647
if (!isTimedOut) {
@@ -3685,12 +3690,17 @@ export function attach(
36853690
}
36863691
} else {
36873692
const contentFiber = fiber.child;
3693+
const suspenseState = fiber.memoizedState;
36883694
if (contentFiber === null) {
3689-
throw new Error(
3690-
'There should always be an Offscreen Fiber child in a Suspense boundary.',
3691-
);
3695+
if (suspenseState === null || suspenseState.dehydrated === null) {
3696+
throw new Error(
3697+
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3698+
);
3699+
}
3700+
// This Suspense Fiber is still dehydrated. It won't have any children
3701+
// until hydration.
36923702
}
3693-
const isTimedOut = fiber.memoizedState !== null;
3703+
const isTimedOut = suspenseState !== null;
36943704
if (!isTimedOut) {
36953705
newSuspenseNode.rects = measureInstance(newInstance);
36963706
}
@@ -3821,37 +3831,42 @@ export function attach(
38213831
// Modern Suspense path
38223832
const contentFiber = fiber.child;
38233833
if (contentFiber === null) {
3824-
throw new Error(
3825-
'There should always be an Offscreen Fiber child in a Suspense boundary.',
3826-
);
3827-
}
3828-
3829-
trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode);
3830-
3831-
const fallbackFiber = contentFiber.sibling;
3834+
const suspenseState = fiber.memoizedState;
3835+
if (suspenseState === null || suspenseState.dehydrated === null) {
3836+
throw new Error(
3837+
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3838+
);
3839+
}
3840+
// This Suspense Fiber is still dehydrated. It won't have any children
3841+
// until hydration.
3842+
} else {
3843+
trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode);
38323844
3833-
// First update only the Offscreen boundary. I.e. the main content.
3834-
mountVirtualChildrenRecursively(
3835-
contentFiber,
3836-
fallbackFiber,
3837-
traceNearestHostComponentUpdate,
3838-
0, // first level
3839-
);
3845+
const fallbackFiber = contentFiber.sibling;
38403846
3841-
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
3842-
// reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
3843-
// Since the fallback conceptually blocks the parent.
3844-
reconcilingParentSuspenseNode = stashedSuspenseParent;
3845-
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3846-
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3847-
shouldPopSuspenseNode = false;
3848-
if (fallbackFiber !== null) {
3847+
// First update only the Offscreen boundary. I.e. the main content.
38493848
mountVirtualChildrenRecursively(
3849+
contentFiber,
38503850
fallbackFiber,
3851-
null,
38523851
traceNearestHostComponentUpdate,
38533852
0, // first level
38543853
);
3854+
3855+
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
3856+
// reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
3857+
// Since the fallback conceptually blocks the parent.
3858+
reconcilingParentSuspenseNode = stashedSuspenseParent;
3859+
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3860+
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3861+
shouldPopSuspenseNode = false;
3862+
if (fallbackFiber !== null) {
3863+
mountVirtualChildrenRecursively(
3864+
fallbackFiber,
3865+
null,
3866+
traceNearestHostComponentUpdate,
3867+
0, // first level
3868+
);
3869+
}
38553870
}
38563871
} else {
38573872
if (fiber.child !== null) {
@@ -4768,68 +4783,84 @@ export function attach(
47684783
const prevContentFiber = prevFiber.child;
47694784
const nextContentFiber = nextFiber.child;
47704785
if (nextContentFiber === null || prevContentFiber === null) {
4771-
throw new Error(
4772-
'There should always be an Offscreen Fiber child in a Suspense boundary.',
4773-
);
4774-
}
4775-
const prevFallbackFiber = prevContentFiber.sibling;
4776-
const nextFallbackFiber = nextContentFiber.sibling;
4786+
const previousSuspenseState = prevFiber.memoizedState;
4787+
const nextSuspenseState = nextFiber.memoizedState;
4788+
if (
4789+
previousSuspenseState === null ||
4790+
previousSuspenseState.dehydrated === null ||
4791+
nextSuspenseState === null ||
4792+
nextSuspenseState.dehydrated === null
4793+
) {
4794+
throw new Error(
4795+
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
4796+
);
4797+
}
4798+
// This Suspense Fiber is still dehydrated. It won't have any children
4799+
// until hydration.
4800+
} else {
4801+
const prevFallbackFiber = prevContentFiber.sibling;
4802+
const nextFallbackFiber = nextContentFiber.sibling;
47774803
4778-
if ((prevFiber.stateNode === null) !== (nextFiber.stateNode === null)) {
4779-
trackThrownPromisesFromRetryCache(
4780-
fiberInstance.suspenseNode,
4781-
nextFiber.stateNode,
4782-
);
4783-
}
4804+
if (
4805+
(prevFiber.stateNode === null) !==
4806+
(nextFiber.stateNode === null)
4807+
) {
4808+
trackThrownPromisesFromRetryCache(
4809+
fiberInstance.suspenseNode,
4810+
nextFiber.stateNode,
4811+
);
4812+
}
47844813
4785-
// First update only the Offscreen boundary. I.e. the main content.
4786-
updateFlags |= updateVirtualChildrenRecursively(
4787-
nextContentFiber,
4788-
nextFallbackFiber,
4789-
prevContentFiber,
4790-
traceNearestHostComponentUpdate,
4791-
0,
4792-
);
4814+
// First update only the Offscreen boundary. I.e. the main content.
4815+
updateFlags |= updateVirtualChildrenRecursively(
4816+
nextContentFiber,
4817+
nextFallbackFiber,
4818+
prevContentFiber,
4819+
traceNearestHostComponentUpdate,
4820+
0,
4821+
);
47934822
4794-
shouldMeasureSuspenseNode = false;
4795-
if (prevFallbackFiber !== null || nextFallbackFiber !== null) {
4796-
const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode;
4797-
const fallbackStashedSuspensePrevious =
4798-
previouslyReconciledSiblingSuspenseNode;
4799-
const fallbackStashedSuspenseRemaining =
4800-
remainingReconcilingChildrenSuspenseNodes;
4801-
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
4802-
// reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
4803-
// Since the fallback conceptually blocks the parent.
4804-
reconcilingParentSuspenseNode = stashedSuspenseParent;
4805-
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
4806-
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
4807-
try {
4808-
if (nextFallbackFiber === null) {
4809-
unmountRemainingChildren();
4810-
} else {
4811-
updateFlags |= updateVirtualChildrenRecursively(
4812-
nextFallbackFiber,
4813-
null,
4814-
prevFallbackFiber,
4815-
traceNearestHostComponentUpdate,
4816-
0,
4817-
);
4818-
}
4819-
} finally {
4820-
reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
4821-
previouslyReconciledSiblingSuspenseNode =
4822-
fallbackStashedSuspensePrevious;
4823+
shouldMeasureSuspenseNode = false;
4824+
if (prevFallbackFiber !== null || nextFallbackFiber !== null) {
4825+
const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode;
4826+
const fallbackStashedSuspensePrevious =
4827+
previouslyReconciledSiblingSuspenseNode;
4828+
const fallbackStashedSuspenseRemaining =
4829+
remainingReconcilingChildrenSuspenseNodes;
4830+
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
4831+
// reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
4832+
// Since the fallback conceptually blocks the parent.
4833+
reconcilingParentSuspenseNode = stashedSuspenseParent;
4834+
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
48234835
remainingReconcilingChildrenSuspenseNodes =
4824-
fallbackStashedSuspenseRemaining;
4836+
stashedSuspenseRemaining;
4837+
try {
4838+
if (nextFallbackFiber === null) {
4839+
unmountRemainingChildren();
4840+
} else {
4841+
updateFlags |= updateVirtualChildrenRecursively(
4842+
nextFallbackFiber,
4843+
null,
4844+
prevFallbackFiber,
4845+
traceNearestHostComponentUpdate,
4846+
0,
4847+
);
4848+
}
4849+
} finally {
4850+
reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
4851+
previouslyReconciledSiblingSuspenseNode =
4852+
fallbackStashedSuspensePrevious;
4853+
remainingReconcilingChildrenSuspenseNodes =
4854+
fallbackStashedSuspenseRemaining;
4855+
}
4856+
}
4857+
if (nextFiber.memoizedState === null) {
4858+
// Measure this Suspense node in case it changed. We don't update the rect while
4859+
// we're inside a disconnected subtree nor if we are the Suspense boundary that
4860+
// is suspended. This lets us keep the rectangle of the displayed content while
4861+
// we're suspended to visualize the resulting state.
4862+
shouldMeasureSuspenseNode = !isInDisconnectedSubtree;
48254863
}
4826-
}
4827-
if (nextFiber.memoizedState === null) {
4828-
// Measure this Suspense node in case it changed. We don't update the rect while
4829-
// we're inside a disconnected subtree nor if we are the Suspense boundary that
4830-
// is suspended. This lets us keep the rectangle of the displayed content while
4831-
// we're suspended to visualize the resulting state.
4832-
shouldMeasureSuspenseNode = !isInDisconnectedSubtree;
48334864
}
48344865
} else {
48354866
// Common case: Primary -> Primary.

0 commit comments

Comments
 (0)