Skip to content

Commit 223a52c

Browse files
committed
Handle hydrated nodes in update path
1 parent 3d11e62 commit 223a52c

File tree

2 files changed

+174
-91
lines changed

2 files changed

+174
-91
lines changed

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

Lines changed: 173 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1543,6 +1543,22 @@ export function attach(
15431543
return Array.from(knownEnvironmentNames);
15441544
}
15451545
1546+
function isFiberHydrated(fiber: Fiber): boolean {
1547+
if (OffscreenComponent === -1) {
1548+
throw new Error('not implemented for legacy suspense');
1549+
}
1550+
switch (fiber.tag) {
1551+
case HostRoot:
1552+
const rootState = fiber.memoizedState;
1553+
return !rootState.isDehydrated;
1554+
case SuspenseComponent:
1555+
const suspenseState = fiber.memoizedState;
1556+
return suspenseState === null || suspenseState.dehydrated === null;
1557+
default:
1558+
throw new Error('not implemented for work tag ' + fiber.tag);
1559+
}
1560+
}
1561+
15461562
function shouldFilterVirtual(
15471563
data: ReactComponentInfo,
15481564
secondaryEnv: null | string,
@@ -3610,6 +3626,50 @@ export function attach(
36103626
);
36113627
}
36123628
3629+
function mountSuspenseChildrenRecursively(
3630+
contentFiber: Fiber,
3631+
traceNearestHostComponentUpdate: boolean,
3632+
stashedSuspenseParent: SuspenseNode | null,
3633+
stashedSuspensePrevious: SuspenseNode | null,
3634+
stashedSuspenseRemaining: SuspenseNode | null,
3635+
) {
3636+
const fallbackFiber = contentFiber.sibling;
3637+
3638+
// First update only the Offscreen boundary. I.e. the main content.
3639+
mountVirtualChildrenRecursively(
3640+
contentFiber,
3641+
fallbackFiber,
3642+
traceNearestHostComponentUpdate,
3643+
0, // first level
3644+
);
3645+
3646+
if (fallbackFiber !== null) {
3647+
const fallbackStashedSuspenseParent = stashedSuspenseParent;
3648+
const fallbackStashedSuspensePrevious = stashedSuspensePrevious;
3649+
const fallbackStashedSuspenseRemaining = stashedSuspenseRemaining;
3650+
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
3651+
// reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
3652+
// Since the fallback conceptually blocks the parent.
3653+
reconcilingParentSuspenseNode = stashedSuspenseParent;
3654+
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3655+
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3656+
try {
3657+
mountVirtualChildrenRecursively(
3658+
fallbackFiber,
3659+
null,
3660+
traceNearestHostComponentUpdate,
3661+
0, // first level
3662+
);
3663+
} finally {
3664+
reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
3665+
previouslyReconciledSiblingSuspenseNode =
3666+
fallbackStashedSuspensePrevious;
3667+
remainingReconcilingChildrenSuspenseNodes =
3668+
fallbackStashedSuspenseRemaining;
3669+
}
3670+
}
3671+
}
3672+
36133673
function mountFiberRecursively(
36143674
fiber: Fiber,
36153675
traceNearestHostComponentUpdate: boolean,
@@ -3632,14 +3692,15 @@ export function attach(
36323692
newSuspenseNode.rects = measureInstance(newInstance);
36333693
}
36343694
} else {
3635-
const contentFiber = fiber.child;
3636-
if (contentFiber === null) {
3637-
const suspenseState = fiber.memoizedState;
3638-
if (suspenseState === null || suspenseState.dehydrated === null) {
3695+
const hydrated = isFiberHydrated(fiber);
3696+
if (hydrated) {
3697+
const contentFiber = fiber.child;
3698+
if (contentFiber === null) {
36393699
throw new Error(
36403700
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
36413701
);
36423702
}
3703+
} else {
36433704
// This Suspense Fiber is still dehydrated. It won't have any children
36443705
// until hydration.
36453706
}
@@ -3689,17 +3750,19 @@ export function attach(
36893750
newSuspenseNode.rects = measureInstance(newInstance);
36903751
}
36913752
} else {
3692-
const contentFiber = fiber.child;
3693-
const suspenseState = fiber.memoizedState;
3694-
if (contentFiber === null) {
3695-
if (suspenseState === null || suspenseState.dehydrated === null) {
3753+
const hydrated = isFiberHydrated(fiber);
3754+
if (hydrated) {
3755+
const contentFiber = fiber.child;
3756+
if (contentFiber === null) {
36963757
throw new Error(
36973758
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
36983759
);
36993760
}
3761+
} else {
37003762
// This Suspense Fiber is still dehydrated. It won't have any children
37013763
// until hydration.
37023764
}
3765+
const suspenseState = fiber.memoizedState;
37033766
const isTimedOut = suspenseState !== null;
37043767
if (!isTimedOut) {
37053768
newSuspenseNode.rects = measureInstance(newInstance);
@@ -3830,43 +3893,26 @@ export function attach(
38303893
) {
38313894
// Modern Suspense path
38323895
const contentFiber = fiber.child;
3833-
if (contentFiber === null) {
3834-
const suspenseState = fiber.memoizedState;
3835-
if (suspenseState === null || suspenseState.dehydrated === null) {
3896+
const hydrated = isFiberHydrated(fiber);
3897+
if (hydrated) {
3898+
if (contentFiber === null) {
38363899
throw new Error(
38373900
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
38383901
);
38393902
}
3840-
// This Suspense Fiber is still dehydrated. It won't have any children
3841-
// until hydration.
3842-
} else {
3843-
trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode);
38443903
3845-
const fallbackFiber = contentFiber.sibling;
3904+
trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode);
38463905
3847-
// First update only the Offscreen boundary. I.e. the main content.
3848-
mountVirtualChildrenRecursively(
3906+
mountSuspenseChildrenRecursively(
38493907
contentFiber,
3850-
fallbackFiber,
38513908
traceNearestHostComponentUpdate,
3852-
0, // first level
3909+
stashedSuspenseParent,
3910+
stashedSuspensePrevious,
3911+
stashedSuspenseRemaining,
38533912
);
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-
}
3913+
} else {
3914+
// This Suspense Fiber is still dehydrated. It won't have any children
3915+
// until hydration.
38703916
}
38713917
} else {
38723918
if (fiber.child !== null) {
@@ -4520,6 +4566,63 @@ export function attach(
45204566
);
45214567
}
45224568
4569+
function updateSuspenseChildrenRecursively(
4570+
nextContentFiber: Fiber,
4571+
prevContentFiber: Fiber,
4572+
traceNearestHostComponentUpdate: boolean,
4573+
stashedSuspenseParent: null | SuspenseNode,
4574+
stashedSuspensePrevious: null | SuspenseNode,
4575+
stashedSuspenseRemaining: null | SuspenseNode,
4576+
): number {
4577+
let updateFlags = NoUpdate;
4578+
const prevFallbackFiber = prevContentFiber.sibling;
4579+
const nextFallbackFiber = nextContentFiber.sibling;
4580+
4581+
// First update only the Offscreen boundary. I.e. the main content.
4582+
updateFlags |= updateVirtualChildrenRecursively(
4583+
nextContentFiber,
4584+
nextFallbackFiber,
4585+
prevContentFiber,
4586+
traceNearestHostComponentUpdate,
4587+
0,
4588+
);
4589+
4590+
if (prevFallbackFiber !== null || nextFallbackFiber !== null) {
4591+
const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode;
4592+
const fallbackStashedSuspensePrevious =
4593+
previouslyReconciledSiblingSuspenseNode;
4594+
const fallbackStashedSuspenseRemaining =
4595+
remainingReconcilingChildrenSuspenseNodes;
4596+
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
4597+
// reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
4598+
// Since the fallback conceptually blocks the parent.
4599+
reconcilingParentSuspenseNode = stashedSuspenseParent;
4600+
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
4601+
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
4602+
try {
4603+
if (nextFallbackFiber === null) {
4604+
unmountRemainingChildren();
4605+
} else {
4606+
updateFlags |= updateVirtualChildrenRecursively(
4607+
nextFallbackFiber,
4608+
null,
4609+
prevFallbackFiber,
4610+
traceNearestHostComponentUpdate,
4611+
0,
4612+
);
4613+
}
4614+
} finally {
4615+
reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
4616+
previouslyReconciledSiblingSuspenseNode =
4617+
fallbackStashedSuspensePrevious;
4618+
remainingReconcilingChildrenSuspenseNodes =
4619+
fallbackStashedSuspenseRemaining;
4620+
}
4621+
}
4622+
4623+
return updateFlags;
4624+
}
4625+
45234626
// Returns whether closest unfiltered fiber parent needs to reset its child list.
45244627
function updateFiberRecursively(
45254628
fiberInstance: null | FiberInstance | FilteredFiberInstance, // null if this should be filtered
@@ -4780,87 +4883,67 @@ export function attach(
47804883
fiberInstance.suspenseNode !== null
47814884
) {
47824885
// Modern Suspense path
4886+
const suspenseNode = fiberInstance.suspenseNode;
47834887
const prevContentFiber = prevFiber.child;
47844888
const nextContentFiber = nextFiber.child;
4785-
if (nextContentFiber === null || prevContentFiber === null) {
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-
) {
4889+
const previousHydrated = isFiberHydrated(prevFiber);
4890+
const nextHydrated = isFiberHydrated(nextFiber);
4891+
if (previousHydrated && nextHydrated) {
4892+
if (nextContentFiber === null || prevContentFiber === null) {
47944893
throw new Error(
47954894
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
47964895
);
47974896
}
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;
48034897
48044898
if (
48054899
(prevFiber.stateNode === null) !==
48064900
(nextFiber.stateNode === null)
48074901
) {
48084902
trackThrownPromisesFromRetryCache(
4809-
fiberInstance.suspenseNode,
4903+
suspenseNode,
48104904
nextFiber.stateNode,
48114905
);
48124906
}
48134907
4814-
// First update only the Offscreen boundary. I.e. the main content.
4815-
updateFlags |= updateVirtualChildrenRecursively(
4908+
shouldMeasureSuspenseNode = false;
4909+
updateFlags |= updateSuspenseChildrenRecursively(
48164910
nextContentFiber,
4817-
nextFallbackFiber,
48184911
prevContentFiber,
48194912
traceNearestHostComponentUpdate,
4820-
0,
4913+
stashedSuspenseParent,
4914+
stashedSuspensePrevious,
4915+
stashedSuspenseRemaining,
48214916
);
4822-
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;
4835-
remainingReconcilingChildrenSuspenseNodes =
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-
}
48574917
if (nextFiber.memoizedState === null) {
48584918
// Measure this Suspense node in case it changed. We don't update the rect while
48594919
// we're inside a disconnected subtree nor if we are the Suspense boundary that
48604920
// is suspended. This lets us keep the rectangle of the displayed content while
48614921
// we're suspended to visualize the resulting state.
48624922
shouldMeasureSuspenseNode = !isInDisconnectedSubtree;
48634923
}
4924+
} else if (!previousHydrated && nextHydrated) {
4925+
if (nextContentFiber === null) {
4926+
throw new Error(
4927+
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
4928+
);
4929+
}
4930+
4931+
trackThrownPromisesFromRetryCache(suspenseNode, nextFiber.stateNode);
4932+
4933+
mountSuspenseChildrenRecursively(
4934+
nextContentFiber,
4935+
traceNearestHostComponentUpdate,
4936+
stashedSuspenseParent,
4937+
stashedSuspensePrevious,
4938+
stashedSuspenseRemaining,
4939+
);
4940+
} else if (previousHydrated && !nextHydrated) {
4941+
throw new Error(
4942+
'Encountered a dehydrated Suspense boundary that was previously hydrated.',
4943+
);
4944+
} else {
4945+
// This Suspense Fiber is still dehydrated. It won't have any children
4946+
// until hydration.
48644947
}
48654948
} else {
48664949
// 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)