Skip to content

Commit c3971d5

Browse files
committed
Handle hydrated nodes in update path
1 parent 3a14dff commit c3971d5

File tree

2 files changed

+168
-89
lines changed

2 files changed

+168
-89
lines changed

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

Lines changed: 167 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,6 +1534,22 @@ export function attach(
15341534
return Array.from(knownEnvironmentNames);
15351535
}
15361536
1537+
function isFiberHydrated(fiber: Fiber): boolean {
1538+
if (OffscreenComponent === -1) {
1539+
throw new Error('not implemented for legacy suspense');
1540+
}
1541+
switch (fiber.tag) {
1542+
case HostRoot:
1543+
const rootState = fiber.memoizedState;
1544+
return !rootState.isDehydrated;
1545+
case SuspenseComponent:
1546+
const suspenseState = fiber.memoizedState;
1547+
return suspenseState === null || suspenseState.dehydrated === null;
1548+
default:
1549+
throw new Error('not implemented for work tag ' + fiber.tag);
1550+
}
1551+
}
1552+
15371553
function shouldFilterVirtual(
15381554
data: ReactComponentInfo,
15391555
secondaryEnv: null | string,
@@ -3579,6 +3595,50 @@ export function attach(
35793595
);
35803596
}
35813597
3598+
function mountSuspenseChildrenRecursively(
3599+
contentFiber: Fiber,
3600+
traceNearestHostComponentUpdate: boolean,
3601+
stashedSuspenseParent: SuspenseNode | null,
3602+
stashedSuspensePrevious: SuspenseNode | null,
3603+
stashedSuspenseRemaining: SuspenseNode | null,
3604+
) {
3605+
const fallbackFiber = contentFiber.sibling;
3606+
3607+
// First update only the Offscreen boundary. I.e. the main content.
3608+
mountVirtualChildrenRecursively(
3609+
contentFiber,
3610+
fallbackFiber,
3611+
traceNearestHostComponentUpdate,
3612+
0, // first level
3613+
);
3614+
3615+
if (fallbackFiber !== null) {
3616+
const fallbackStashedSuspenseParent = stashedSuspenseParent;
3617+
const fallbackStashedSuspensePrevious = stashedSuspensePrevious;
3618+
const fallbackStashedSuspenseRemaining = stashedSuspenseRemaining;
3619+
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
3620+
// reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
3621+
// Since the fallback conceptually blocks the parent.
3622+
reconcilingParentSuspenseNode = stashedSuspenseParent;
3623+
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3624+
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3625+
try {
3626+
mountVirtualChildrenRecursively(
3627+
fallbackFiber,
3628+
null,
3629+
traceNearestHostComponentUpdate,
3630+
0, // first level
3631+
);
3632+
} finally {
3633+
reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
3634+
previouslyReconciledSiblingSuspenseNode =
3635+
fallbackStashedSuspensePrevious;
3636+
remainingReconcilingChildrenSuspenseNodes =
3637+
fallbackStashedSuspenseRemaining;
3638+
}
3639+
}
3640+
}
3641+
35823642
function mountFiberRecursively(
35833643
fiber: Fiber,
35843644
traceNearestHostComponentUpdate: boolean,
@@ -3601,14 +3661,15 @@ export function attach(
36013661
newSuspenseNode.rects = measureInstance(newInstance);
36023662
}
36033663
} else {
3604-
const contentFiber = fiber.child;
3605-
if (contentFiber === null) {
3606-
const suspenseState = fiber.memoizedState;
3607-
if (suspenseState === null || suspenseState.dehydrated === null) {
3664+
const hydrated = isFiberHydrated(fiber);
3665+
if (hydrated) {
3666+
const contentFiber = fiber.child;
3667+
if (contentFiber === null) {
36083668
throw new Error(
36093669
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
36103670
);
36113671
}
3672+
} else {
36123673
// This Suspense Fiber is still dehydrated. It won't have any children
36133674
// until hydration.
36143675
}
@@ -3658,17 +3719,19 @@ export function attach(
36583719
newSuspenseNode.rects = measureInstance(newInstance);
36593720
}
36603721
} else {
3661-
const contentFiber = fiber.child;
3662-
const suspenseState = fiber.memoizedState;
3663-
if (contentFiber === null) {
3664-
if (suspenseState === null || suspenseState.dehydrated === null) {
3722+
const hydrated = isFiberHydrated(fiber);
3723+
if (hydrated) {
3724+
const contentFiber = fiber.child;
3725+
if (contentFiber === null) {
36653726
throw new Error(
36663727
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
36673728
);
36683729
}
3730+
} else {
36693731
// This Suspense Fiber is still dehydrated. It won't have any children
36703732
// until hydration.
36713733
}
3734+
const suspenseState = fiber.memoizedState;
36723735
const isTimedOut = suspenseState !== null;
36733736
if (!isTimedOut) {
36743737
newSuspenseNode.rects = measureInstance(newInstance);
@@ -3796,41 +3859,24 @@ export function attach(
37963859
) {
37973860
// Modern Suspense path
37983861
const contentFiber = fiber.child;
3799-
if (contentFiber === null) {
3800-
const suspenseState = fiber.memoizedState;
3801-
if (suspenseState === null || suspenseState.dehydrated === null) {
3862+
const hydrated = isFiberHydrated(fiber);
3863+
if (hydrated) {
3864+
if (contentFiber === null) {
38023865
throw new Error(
38033866
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
38043867
);
38053868
}
3806-
// This Suspense Fiber is still dehydrated. It won't have any children
3807-
// until hydration.
3808-
} else {
3809-
const fallbackFiber = contentFiber.sibling;
38103869
3811-
// First update only the Offscreen boundary. I.e. the main content.
3812-
mountVirtualChildrenRecursively(
3870+
mountSuspenseChildrenRecursively(
38133871
contentFiber,
3814-
fallbackFiber,
38153872
traceNearestHostComponentUpdate,
3816-
0, // first level
3873+
stashedSuspenseParent,
3874+
stashedSuspensePrevious,
3875+
stashedSuspenseRemaining,
38173876
);
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-
}
3877+
} else {
3878+
// This Suspense Fiber is still dehydrated. It won't have any children
3879+
// until hydration.
38343880
}
38353881
} else {
38363882
if (fiber.child !== null) {
@@ -4484,6 +4530,63 @@ export function attach(
44844530
);
44854531
}
44864532
4533+
function updateSuspenseChildrenRecursively(
4534+
nextContentFiber: Fiber,
4535+
prevContentFiber: Fiber,
4536+
traceNearestHostComponentUpdate: boolean,
4537+
stashedSuspenseParent: null | SuspenseNode,
4538+
stashedSuspensePrevious: null | SuspenseNode,
4539+
stashedSuspenseRemaining: null | SuspenseNode,
4540+
): number {
4541+
let updateFlags = NoUpdate;
4542+
const prevFallbackFiber = prevContentFiber.sibling;
4543+
const nextFallbackFiber = nextContentFiber.sibling;
4544+
4545+
// First update only the Offscreen boundary. I.e. the main content.
4546+
updateFlags |= updateVirtualChildrenRecursively(
4547+
nextContentFiber,
4548+
nextFallbackFiber,
4549+
prevContentFiber,
4550+
traceNearestHostComponentUpdate,
4551+
0,
4552+
);
4553+
4554+
if (prevFallbackFiber !== null || nextFallbackFiber !== null) {
4555+
const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode;
4556+
const fallbackStashedSuspensePrevious =
4557+
previouslyReconciledSiblingSuspenseNode;
4558+
const fallbackStashedSuspenseRemaining =
4559+
remainingReconcilingChildrenSuspenseNodes;
4560+
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
4561+
// reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
4562+
// Since the fallback conceptually blocks the parent.
4563+
reconcilingParentSuspenseNode = stashedSuspenseParent;
4564+
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
4565+
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
4566+
try {
4567+
if (nextFallbackFiber === null) {
4568+
unmountRemainingChildren();
4569+
} else {
4570+
updateFlags |= updateVirtualChildrenRecursively(
4571+
nextFallbackFiber,
4572+
null,
4573+
prevFallbackFiber,
4574+
traceNearestHostComponentUpdate,
4575+
0,
4576+
);
4577+
}
4578+
} finally {
4579+
reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
4580+
previouslyReconciledSiblingSuspenseNode =
4581+
fallbackStashedSuspensePrevious;
4582+
remainingReconcilingChildrenSuspenseNodes =
4583+
fallbackStashedSuspenseRemaining;
4584+
}
4585+
}
4586+
4587+
return updateFlags;
4588+
}
4589+
44874590
// Returns whether closest unfiltered fiber parent needs to reset its child list.
44884591
function updateFiberRecursively(
44894592
fiberInstance: null | FiberInstance | FilteredFiberInstance, // null if this should be filtered
@@ -4734,75 +4837,51 @@ export function attach(
47344837
// Modern Suspense path
47354838
const prevContentFiber = prevFiber.child;
47364839
const nextContentFiber = nextFiber.child;
4737-
if (nextContentFiber === null || prevContentFiber === null) {
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-
) {
4840+
const previousHydrated = isFiberHydrated(prevFiber);
4841+
const nextHydrated = isFiberHydrated(nextFiber);
4842+
if (previousHydrated && nextHydrated) {
4843+
if (nextContentFiber === null || prevContentFiber === null) {
47464844
throw new Error(
47474845
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
47484846
);
47494847
}
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;
47554848
4756-
// First update only the Offscreen boundary. I.e. the main content.
4757-
updateFlags |= updateVirtualChildrenRecursively(
4849+
shouldMeasureSuspenseNode = false;
4850+
updateFlags |= updateSuspenseChildrenRecursively(
47584851
nextContentFiber,
4759-
nextFallbackFiber,
47604852
prevContentFiber,
47614853
traceNearestHostComponentUpdate,
4762-
0,
4854+
stashedSuspenseParent,
4855+
stashedSuspensePrevious,
4856+
stashedSuspenseRemaining,
47634857
);
4764-
4765-
shouldMeasureSuspenseNode = false;
4766-
if (prevFallbackFiber !== null || 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;
4777-
remainingReconcilingChildrenSuspenseNodes =
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-
}
47994858
if (nextFiber.memoizedState === null) {
48004859
// Measure this Suspense node in case it changed. We don't update the rect while
48014860
// we're inside a disconnected subtree nor if we are the Suspense boundary that
48024861
// is suspended. This lets us keep the rectangle of the displayed content while
48034862
// we're suspended to visualize the resulting state.
48044863
shouldMeasureSuspenseNode = !isInDisconnectedSubtree;
48054864
}
4865+
} else if (!previousHydrated && nextHydrated) {
4866+
if (nextContentFiber === null) {
4867+
throw new Error(
4868+
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
4869+
);
4870+
}
4871+
mountSuspenseChildrenRecursively(
4872+
nextContentFiber,
4873+
traceNearestHostComponentUpdate,
4874+
stashedSuspenseParent,
4875+
stashedSuspensePrevious,
4876+
stashedSuspenseRemaining,
4877+
);
4878+
} else if (previousHydrated && !nextHydrated) {
4879+
throw new Error(
4880+
'Encountered a dehydrated Suspense boundary that was previously hydrated.',
4881+
);
4882+
} else {
4883+
// This Suspense Fiber is still dehydrated. It won't have any children
4884+
// until hydration.
48064885
}
48074886
} else {
48084887
// Common case: Primary -> Primary.

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1796,7 +1796,7 @@ function updateHostRoot(
17961796
}
17971797

17981798
const nextProps = workInProgress.pendingProps;
1799-
const prevState = workInProgress.memoizedState;
1799+
const prevState: RootState = workInProgress.memoizedState;
18001800
const prevChildren = prevState.element;
18011801
cloneUpdateQueue(current, workInProgress);
18021802
processUpdateQueue(workInProgress, nextProps, null, renderLanes);

0 commit comments

Comments
 (0)