Skip to content
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
319 changes: 214 additions & 105 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1543,6 +1543,22 @@ export function attach(
return Array.from(knownEnvironmentNames);
}

function isFiberHydrated(fiber: Fiber): boolean {
if (OffscreenComponent === -1) {
throw new Error('not implemented for legacy suspense');
}
switch (fiber.tag) {
case HostRoot:
const rootState = fiber.memoizedState;
return !rootState.isDehydrated;
case SuspenseComponent:
const suspenseState = fiber.memoizedState;
return suspenseState === null || suspenseState.dehydrated === null;
default:
throw new Error('not implemented for work tag ' + fiber.tag);
}
}

function shouldFilterVirtual(
data: ReactComponentInfo,
secondaryEnv: null | string,
Expand Down Expand Up @@ -3610,6 +3626,50 @@ export function attach(
);
}

function mountSuspenseChildrenRecursively(
contentFiber: Fiber,
traceNearestHostComponentUpdate: boolean,
stashedSuspenseParent: SuspenseNode | null,
stashedSuspensePrevious: SuspenseNode | null,
stashedSuspenseRemaining: SuspenseNode | null,
) {
const fallbackFiber = contentFiber.sibling;

// First update only the Offscreen boundary. I.e. the main content.
mountVirtualChildrenRecursively(
contentFiber,
fallbackFiber,
traceNearestHostComponentUpdate,
0, // first level
);

if (fallbackFiber !== null) {
const fallbackStashedSuspenseParent = stashedSuspenseParent;
const fallbackStashedSuspensePrevious = stashedSuspensePrevious;
const fallbackStashedSuspenseRemaining = stashedSuspenseRemaining;
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
// reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
// Since the fallback conceptually blocks the parent.
reconcilingParentSuspenseNode = stashedSuspenseParent;
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
try {
mountVirtualChildrenRecursively(
fallbackFiber,
null,
traceNearestHostComponentUpdate,
0, // first level
);
} finally {
reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
previouslyReconciledSiblingSuspenseNode =
fallbackStashedSuspensePrevious;
remainingReconcilingChildrenSuspenseNodes =
fallbackStashedSuspenseRemaining;
}
}
}

function mountFiberRecursively(
fiber: Fiber,
traceNearestHostComponentUpdate: boolean,
Expand All @@ -3632,11 +3692,17 @@ export function attach(
newSuspenseNode.rects = measureInstance(newInstance);
}
} else {
const contentFiber = fiber.child;
if (contentFiber === null) {
throw new Error(
'There should always be an Offscreen Fiber child in a Suspense boundary.',
);
const hydrated = isFiberHydrated(fiber);
if (hydrated) {
const contentFiber = fiber.child;
if (contentFiber === null) {
throw new Error(
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
);
}
} else {
// This Suspense Fiber is still dehydrated. It won't have any children
// until hydration.
}
const isTimedOut = fiber.memoizedState !== null;
if (!isTimedOut) {
Expand Down Expand Up @@ -3684,13 +3750,20 @@ export function attach(
newSuspenseNode.rects = measureInstance(newInstance);
}
} else {
const contentFiber = fiber.child;
if (contentFiber === null) {
throw new Error(
'There should always be an Offscreen Fiber child in a Suspense boundary.',
);
const hydrated = isFiberHydrated(fiber);
if (hydrated) {
const contentFiber = fiber.child;
if (contentFiber === null) {
throw new Error(
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
);
}
} else {
// This Suspense Fiber is still dehydrated. It won't have any children
// until hydration.
}
const isTimedOut = fiber.memoizedState !== null;
const suspenseState = fiber.memoizedState;
const isTimedOut = suspenseState !== null;
if (!isTimedOut) {
newSuspenseNode.rects = measureInstance(newInstance);
}
Expand Down Expand Up @@ -3820,38 +3893,26 @@ export function attach(
) {
// Modern Suspense path
const contentFiber = fiber.child;
if (contentFiber === null) {
throw new Error(
'There should always be an Offscreen Fiber child in a Suspense boundary.',
);
}

trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode);

const fallbackFiber = contentFiber.sibling;
const hydrated = isFiberHydrated(fiber);
if (hydrated) {
if (contentFiber === null) {
throw new Error(
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
);
}

// First update only the Offscreen boundary. I.e. the main content.
mountVirtualChildrenRecursively(
contentFiber,
fallbackFiber,
traceNearestHostComponentUpdate,
0, // first level
);
trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode);

// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
// reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
// Since the fallback conceptually blocks the parent.
reconcilingParentSuspenseNode = stashedSuspenseParent;
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
shouldPopSuspenseNode = false;
if (fallbackFiber !== null) {
mountVirtualChildrenRecursively(
fallbackFiber,
null,
mountSuspenseChildrenRecursively(
contentFiber,
traceNearestHostComponentUpdate,
0, // first level
stashedSuspenseParent,
stashedSuspensePrevious,
stashedSuspenseRemaining,
);
} else {
// This Suspense Fiber is still dehydrated. It won't have any children
// until hydration.
}
} else {
if (fiber.child !== null) {
Expand Down Expand Up @@ -4505,6 +4566,63 @@ export function attach(
);
}

function updateSuspenseChildrenRecursively(
nextContentFiber: Fiber,
prevContentFiber: Fiber,
traceNearestHostComponentUpdate: boolean,
stashedSuspenseParent: null | SuspenseNode,
stashedSuspensePrevious: null | SuspenseNode,
stashedSuspenseRemaining: null | SuspenseNode,
): number {
let updateFlags = NoUpdate;
const prevFallbackFiber = prevContentFiber.sibling;
const nextFallbackFiber = nextContentFiber.sibling;

// First update only the Offscreen boundary. I.e. the main content.
updateFlags |= updateVirtualChildrenRecursively(
nextContentFiber,
nextFallbackFiber,
prevContentFiber,
traceNearestHostComponentUpdate,
0,
);

if (prevFallbackFiber !== null || nextFallbackFiber !== null) {
const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode;
const fallbackStashedSuspensePrevious =
previouslyReconciledSiblingSuspenseNode;
const fallbackStashedSuspenseRemaining =
remainingReconcilingChildrenSuspenseNodes;
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
// reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
// Since the fallback conceptually blocks the parent.
reconcilingParentSuspenseNode = stashedSuspenseParent;
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
try {
if (nextFallbackFiber === null) {
unmountRemainingChildren();
} else {
updateFlags |= updateVirtualChildrenRecursively(
nextFallbackFiber,
null,
prevFallbackFiber,
traceNearestHostComponentUpdate,
0,
);
}
} finally {
reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
previouslyReconciledSiblingSuspenseNode =
fallbackStashedSuspensePrevious;
remainingReconcilingChildrenSuspenseNodes =
fallbackStashedSuspenseRemaining;
}
}

return updateFlags;
}

// Returns whether closest unfiltered fiber parent needs to reset its child list.
function updateFiberRecursively(
fiberInstance: null | FiberInstance | FilteredFiberInstance, // null if this should be filtered
Expand Down Expand Up @@ -4765,71 +4883,67 @@ export function attach(
fiberInstance.suspenseNode !== null
) {
// Modern Suspense path
const suspenseNode = fiberInstance.suspenseNode;
const prevContentFiber = prevFiber.child;
const nextContentFiber = nextFiber.child;
if (nextContentFiber === null || prevContentFiber === null) {
throw new Error(
'There should always be an Offscreen Fiber child in a Suspense boundary.',
);
}
const prevFallbackFiber = prevContentFiber.sibling;
const nextFallbackFiber = nextContentFiber.sibling;
const previousHydrated = isFiberHydrated(prevFiber);
const nextHydrated = isFiberHydrated(nextFiber);
if (previousHydrated && nextHydrated) {
if (nextContentFiber === null || prevContentFiber === null) {
throw new Error(
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
);
}

if ((prevFiber.stateNode === null) !== (nextFiber.stateNode === null)) {
trackThrownPromisesFromRetryCache(
fiberInstance.suspenseNode,
nextFiber.stateNode,
if (
(prevFiber.stateNode === null) !==
(nextFiber.stateNode === null)
) {
trackThrownPromisesFromRetryCache(
suspenseNode,
nextFiber.stateNode,
);
}

shouldMeasureSuspenseNode = false;
updateFlags |= updateSuspenseChildrenRecursively(
nextContentFiber,
prevContentFiber,
traceNearestHostComponentUpdate,
stashedSuspenseParent,
stashedSuspensePrevious,
stashedSuspenseRemaining,
);
}
if (nextFiber.memoizedState === null) {
// Measure this Suspense node in case it changed. We don't update the rect while
// we're inside a disconnected subtree nor if we are the Suspense boundary that
// is suspended. This lets us keep the rectangle of the displayed content while
// we're suspended to visualize the resulting state.
shouldMeasureSuspenseNode = !isInDisconnectedSubtree;
}
} else if (!previousHydrated && nextHydrated) {
if (nextContentFiber === null) {
throw new Error(
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
);
}

// First update only the Offscreen boundary. I.e. the main content.
updateFlags |= updateVirtualChildrenRecursively(
nextContentFiber,
nextFallbackFiber,
prevContentFiber,
traceNearestHostComponentUpdate,
0,
);
trackThrownPromisesFromRetryCache(suspenseNode, nextFiber.stateNode);

shouldMeasureSuspenseNode = false;
if (prevFallbackFiber !== null || nextFallbackFiber !== null) {
const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode;
const fallbackStashedSuspensePrevious =
previouslyReconciledSiblingSuspenseNode;
const fallbackStashedSuspenseRemaining =
remainingReconcilingChildrenSuspenseNodes;
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
// reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
// Since the fallback conceptually blocks the parent.
reconcilingParentSuspenseNode = stashedSuspenseParent;
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
try {
if (nextFallbackFiber === null) {
unmountRemainingChildren();
} else {
updateFlags |= updateVirtualChildrenRecursively(
nextFallbackFiber,
null,
prevFallbackFiber,
traceNearestHostComponentUpdate,
0,
);
}
} finally {
reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
previouslyReconciledSiblingSuspenseNode =
fallbackStashedSuspensePrevious;
remainingReconcilingChildrenSuspenseNodes =
fallbackStashedSuspenseRemaining;
}
}
if (nextFiber.memoizedState === null) {
// Measure this Suspense node in case it changed. We don't update the rect while
// we're inside a disconnected subtree nor if we are the Suspense boundary that
// is suspended. This lets us keep the rectangle of the displayed content while
// we're suspended to visualize the resulting state.
shouldMeasureSuspenseNode = !isInDisconnectedSubtree;
mountSuspenseChildrenRecursively(
nextContentFiber,
traceNearestHostComponentUpdate,
stashedSuspenseParent,
stashedSuspensePrevious,
stashedSuspenseRemaining,
);
} else if (previousHydrated && !nextHydrated) {
throw new Error(
'Encountered a dehydrated Suspense boundary that was previously hydrated.',
);
} else {
// This Suspense Fiber is still dehydrated. It won't have any children
// until hydration.
}
} else {
// Common case: Primary -> Primary.
Expand Down Expand Up @@ -5164,14 +5278,9 @@ export function attach(
// TODO: relying on this seems a bit fishy.
const wasMounted =
prevFiber.memoizedState != null &&
prevFiber.memoizedState.element != null &&
// A dehydrated root is not considered mounted
prevFiber.memoizedState.isDehydrated !== true;
prevFiber.memoizedState.element != null;
const isMounted =
current.memoizedState != null &&
current.memoizedState.element != null &&
// A dehydrated root is not considered mounted
current.memoizedState.isDehydrated !== true;
current.memoizedState != null && current.memoizedState.element != null;
if (!wasMounted && isMounted) {
// Mount a new root.
setRootPseudoKey(currentRoot.id, current);
Expand Down
Loading
Loading