@samitouri / QOS-React / commits / 587f03c922

[DevTools] Build up DevTools Instance Shadow Tree (#30625)

This is the beginning of a refactor of the DevTools Fiber backend. The new approach is basically that we listen to each commit from Fiber and traverse the tree - building up a filtered shadow tree. Then we send diffs based on that tree and perform our own operations against that instead of using Fibers as the source of truth. Fiber diffs Elements -> Fibers. The backend diffs Fibers -> DevToolsInstances as a side-effect it sends deltas to the front end. This makes the algorithm resilient to a different Fiber implementation that doesn't use pairs of Fibers (alternates) but instead stateless new clones each time. In that world we can't treat Fibers as instances. They can hold onto instances but they're not necessarily 1:1 themselves. The same thing also applies to Server Components that don't have their own instances. The algorithm is more or less the same as React's reconciliation in ReactChildFiber itself. However, we do a mutable update of the tree as we go. We also cheat a bit here in the first version in that we still have fiberToFiberInstance map and alternate which makes reorders easier. Further down we could do the reorders by adding the previous set to a temporary map like ChildFiber does but only if they're not already in order. This first bit is just about making sure that we produce correct trees. We have fairly good test coverage already of that already. In the next few follow ups I'll start simplifying the rest of the logic by taking advantage of the new tree.

Sebastian Markbåge committed Aug 8, 2024 at 12:44 UTC 587f03c922cc10f14e66e85d6ea7e4c647ac4bef
1 file changed +414 -236
packages/react-devtools-shared/src/backend/fiber/renderer.js
+414 -236
@@ -147,7 +147,10 @@ const FORCE_ERROR_RESET = /* */ 0b100;
147 type FiberInstance = {
148 kind: 0,
149 id: number,
150 - parent: null | DevToolsInstance, // virtual parent
150 + parent: null | DevToolsInstance, // filtered parent, including virtual
151 + firstChild: null | DevToolsInstance, // filtered first child, including virtual
152 + previousSibling: null | DevToolsInstance, // filtered next sibling, including virtual
153 + nextSibling: null | DevToolsInstance, // filtered next sibling, including virtual
154 flags: number, // Force Error/Suspense
155 componentStack: null | string,
156 errors: null | Map<string, number>, // error messages and count
@@ -157,9 +160,12 @@ type FiberInstance = {
160
161 function createFiberInstance(fiber: Fiber): FiberInstance {
162 return {
160 - kind: 0,
163 + kind: FIBER_INSTANCE,
164 id: getUID(),
165 parent: null,
166 + firstChild: null,
167 + previousSibling: null,
168 + nextSibling: null,
169 flags: 0,
170 componentStack: null,
171 errors: null,
@@ -176,7 +182,10 @@ function createFiberInstance(fiber: Fiber): FiberInstance {
182 type VirtualInstance = {
183 kind: 1,
184 id: number,
179 - parent: null | DevToolsInstance, // virtual parent
185 + parent: null | DevToolsInstance, // filtered parent, including virtual
186 + firstChild: null | DevToolsInstance, // filtered first child, including virtual
187 + previousSibling: null | DevToolsInstance, // filtered next sibling, including virtual
188 + nextSibling: null | DevToolsInstance, // filtered next sibling, including virtual
189 flags: number,
190 componentStack: null | string,
191 // Errors and Warnings happen per ReactComponentInfo which can appear in
@@ -1031,6 +1040,30 @@ export function attach(
1040 }
1041 };
1042
1043 + // eslint-disable-next-line no-unused-vars
1044 + function debugTree(instance: DevToolsInstance, indent: number = 0) {
1045 + if (__DEBUG__) {
1046 + const name =
1047 + (instance.kind === FIBER_INSTANCE
1048 + ? getDisplayNameForFiber(instance.data)
1049 + : instance.data.name) || '';
1050 + console.log(
1051 + ' '.repeat(indent) + '- ' + instance.id + ' (' + name + ')',
1052 + 'parent',
1053 + instance.parent === null ? ' ' : instance.parent.id,
1054 + 'prev',
1055 + instance.previousSibling === null ? ' ' : instance.previousSibling.id,
1056 + 'next',
1057 + instance.nextSibling === null ? ' ' : instance.nextSibling.id,
1058 + );
1059 + let child = instance.firstChild;
1060 + while (child !== null) {
1061 + debugTree(child, indent + 1);
1062 + child = child.nextSibling;
1063 + }
1064 + }
1065 + }
1066 +
1067 // Configurable Components tree filters.
1068 const hideElementsWithDisplayNames: Set<RegExp> = new Set();
1069 const hideElementsWithPaths: Set<RegExp> = new Set();
@@ -1128,7 +1161,7 @@ export function attach(
1161 hook.getFiberRoots(rendererID).forEach(root => {
1162 currentRootID = getOrGenerateFiberInstance(root.current).id;
1163 setRootPseudoKey(currentRootID, root.current);
1131 - mountFiberRecursively(root.current, null, false);
1164 + mountFiberRecursively(root.current, false);
1165 flushPendingEvents(root);
1166 currentRootID = -1;
1167 });
@@ -2096,9 +2129,6 @@ export function attach(
2129 debug('recordMount()', fiber, parentInstance);
2130 }
2131
2099 - // We're placing it in its parent below.
2100 - fiberInstance.parent = parentInstance;
2101 -
2132 const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner');
2133 const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
2134
@@ -2197,7 +2227,10 @@ export function attach(
2227 return fiberInstance;
2228 }
2229
2200 - function recordUnmount(fiber: Fiber, isSimulated: boolean) {
2230 + function recordUnmount(
2231 + fiber: Fiber,
2232 + isSimulated: boolean,
2233 + ): null | FiberInstance {
2234 if (__DEBUG__) {
2235 debug(
2236 'recordUnmount()',
@@ -2228,12 +2261,9 @@ export function attach(
2261 // This also might indicate a Fast Refresh force-remount scenario.
2262 //
2263 // TODO: This is fragile and can obscure actual bugs.
2231 - return;
2264 + return null;
2265 }
2266
2234 - // We're about to remove this from its parent.
2235 - fiberInstance.parent = null;
2236 -
2267 const id = fiberInstance.id;
2268 const isRoot = fiber.tag === HostRoot;
2269 if (isRoot) {
@@ -2260,29 +2290,94 @@ export function attach(
2290 idToTreeBaseDurationMap.delete(id);
2291 }
2292 }
2293 + return fiberInstance;
2294 + }
2295 +
2296 + // Running state of the remaining children from the previous version of this parent that
2297 + // we haven't yet added back. This should be reset anytime we change parent.
2298 + // Any remaining ones at the end will be deleted.
2299 + let remainingReconcilingChildren: null | DevToolsInstance = null;
2300 + // The previously placed child.
2301 + let previouslyReconciledSibling: null | DevToolsInstance = null;
2302 + // To save on stack allocation and ensure that they are updated as a pair, we also store
2303 + // the current parent here as well.
2304 + let reconcilingParent: null | DevToolsInstance = null;
2305 +
2306 + function insertChild(instance: DevToolsInstance): void {
2307 + const parentInstance = reconcilingParent;
2308 + if (parentInstance === null) {
2309 + // This instance is at the root.
2310 + return;
2311 + }
2312 + // Place it in the parent.
2313 + instance.parent = parentInstance;
2314 + if (previouslyReconciledSibling === null) {
2315 + previouslyReconciledSibling = instance;
2316 + parentInstance.firstChild = instance;
2317 + instance.previousSibling = null;
2318 + } else {
2319 + previouslyReconciledSibling.nextSibling = instance;
2320 + instance.previousSibling = previouslyReconciledSibling;
2321 + previouslyReconciledSibling = instance;
2322 + }
2323 + instance.nextSibling = null;
2324 + }
2325 +
2326 + function moveChild(instance: DevToolsInstance): void {
2327 + removeChild(instance);
2328 + insertChild(instance);
2329 + }
2330 +
2331 + function removeChild(instance: DevToolsInstance): void {
2332 + if (instance.parent === null) {
2333 + // Already deleted.
2334 + return;
2335 + }
2336 + const parentInstance = reconcilingParent;
2337 + if (parentInstance === null) {
2338 + throw new Error('Should not have a parent if we are at the root');
2339 + }
2340 + if (instance.parent !== parentInstance) {
2341 + throw new Error(
2342 + 'Cannot remove a node from a different parent than is being reconciled.',
2343 + );
2344 + }
2345 + // Remove an existing child from its current position, which we assume is in the
2346 + // remainingReconcilingChildren set.
2347 + if (instance.previousSibling === null) {
2348 + // We're first in the remaining set. Remove us.
2349 + if (remainingReconcilingChildren !== instance) {
2350 + throw new Error(
2351 + 'Expected a placed child to be moved from the remaining set.',
2352 + );
2353 + }
2354 + remainingReconcilingChildren = instance.nextSibling;
2355 + } else {
2356 + instance.previousSibling.nextSibling = instance.nextSibling;
2357 + }
2358 + if (instance.nextSibling !== null) {
2359 + instance.nextSibling.previousSibling = instance.previousSibling;
2360 + }
2361 + instance.nextSibling = null;
2362 + instance.previousSibling = null;
2363 + instance.parent = null;
2364 }
2365
2366 function mountChildrenRecursively(
2367 firstChild: Fiber,
2267 - parentInstance: DevToolsInstance | null,
2368 traceNearestHostComponentUpdate: boolean,
2369 ): void {
2370 // Iterate over siblings rather than recursing.
2371 // This reduces the chance of stack overflow for wide trees (e.g. lists with many items).
2372 let fiber: Fiber | null = firstChild;
2373 while (fiber !== null) {
2274 - mountFiberRecursively(
2275 - fiber,
2276 - parentInstance,
2277 - traceNearestHostComponentUpdate,
2278 - );
2374 + mountFiberRecursively(fiber, traceNearestHostComponentUpdate);
2375 fiber = fiber.sibling;
2376 }
2377 }
2378
2379 function mountFiberRecursively(
2380 fiber: Fiber,
2285 - parentInstance: DevToolsInstance | null,
2381 traceNearestHostComponentUpdate: boolean,
2382 ): void {
2383 // Generate an ID even for filtered Fibers, in case it's needed later (e.g. for Profiling).
@@ -2290,7 +2385,7 @@ export function attach(
2385 getOrGenerateFiberInstance(fiber);
2386
2387 if (__DEBUG__) {
2293 - debug('mountFiberRecursively()', fiber, parentInstance);
2388 + debug('mountFiberRecursively()', fiber, reconcilingParent);
2389 }
2390
2391 // If we have the tree selection from previous reload, try to match this Fiber.
@@ -2299,72 +2394,87 @@ export function attach(
2394 updateTrackedPathStateBeforeMount(fiber);
2395
2396 const shouldIncludeInTree = !shouldFilterFiber(fiber);
2302 - const newParentInstance = shouldIncludeInTree
2303 - ? recordMount(fiber, parentInstance)
2304 - : parentInstance;
2305 -
2306 - if (traceUpdatesEnabled) {
2307 - if (traceNearestHostComponentUpdate) {
2308 - const elementType = getElementTypeForFiber(fiber);
2309 - // If an ancestor updated, we should mark the nearest host nodes for highlighting.
2310 - if (elementType === ElementTypeHostComponent) {
2311 - traceUpdatesForNodes.add(fiber.stateNode);
2312 - traceNearestHostComponentUpdate = false;
2397 + let newInstance = null;
2398 + if (shouldIncludeInTree) {
2399 + newInstance = recordMount(fiber, reconcilingParent);
2400 + insertChild(newInstance);
2401 + }
2402 + const stashedParent = reconcilingParent;
2403 + const stashedPrevious = previouslyReconciledSibling;
2404 + const stashedRemaining = remainingReconcilingChildren;
2405 + if (shouldIncludeInTree) {
2406 + // Push a new DevTools instance parent while reconciling this subtree.
2407 + reconcilingParent = newInstance;
2408 + previouslyReconciledSibling = null;
2409 + remainingReconcilingChildren = null;
2410 + }
2411 + try {
2412 + if (traceUpdatesEnabled) {
2413 + if (traceNearestHostComponentUpdate) {
2414 + const elementType = getElementTypeForFiber(fiber);
2415 + // If an ancestor updated, we should mark the nearest host nodes for highlighting.
2416 + if (elementType === ElementTypeHostComponent) {
2417 + traceUpdatesForNodes.add(fiber.stateNode);
2418 + traceNearestHostComponentUpdate = false;
2419 + }
2420 }
2314 - }
2421
2316 - // We intentionally do not re-enable the traceNearestHostComponentUpdate flag in this branch,
2317 - // because we don't want to highlight every host node inside of a newly mounted subtree.
2318 - }
2422 + // We intentionally do not re-enable the traceNearestHostComponentUpdate flag in this branch,
2423 + // because we don't want to highlight every host node inside of a newly mounted subtree.
2424 + }
2425
2320 - if (fiber.tag === HostHoistable) {
2321 - aquireHostResource(fiber, fiber.memoizedState);
2322 - }
2426 + if (fiber.tag === HostHoistable) {
2427 + aquireHostResource(fiber, fiber.memoizedState);
2428 + }
2429
2324 - if (fiber.tag === SuspenseComponent) {
2325 - const isTimedOut = fiber.memoizedState !== null;
2326 - if (isTimedOut) {
2327 - // Special case: if Suspense mounts in a timed-out state,
2328 - // get the fallback child from the inner fragment and mount
2329 - // it as if it was our own child. Updates handle this too.
2330 - const primaryChildFragment = fiber.child;
2331 - const fallbackChildFragment = primaryChildFragment
2332 - ? primaryChildFragment.sibling
2333 - : null;
2334 - const fallbackChild = fallbackChildFragment
2335 - ? fallbackChildFragment.child
2336 - : null;
2337 - if (fallbackChild !== null) {
2338 - mountChildrenRecursively(
2339 - fallbackChild,
2340 - newParentInstance,
2341 - traceNearestHostComponentUpdate,
2342 - );
2430 + if (fiber.tag === SuspenseComponent) {
2431 + const isTimedOut = fiber.memoizedState !== null;
2432 + if (isTimedOut) {
2433 + // Special case: if Suspense mounts in a timed-out state,
2434 + // get the fallback child from the inner fragment and mount
2435 + // it as if it was our own child. Updates handle this too.
2436 + const primaryChildFragment = fiber.child;
2437 + const fallbackChildFragment = primaryChildFragment
2438 + ? primaryChildFragment.sibling
2439 + : null;
2440 + const fallbackChild = fallbackChildFragment
2441 + ? fallbackChildFragment.child
2442 + : null;
2443 + if (fallbackChild !== null) {
2444 + mountChildrenRecursively(
2445 + fallbackChild,
2446 + traceNearestHostComponentUpdate,
2447 + );
2448 + }
2449 + } else {
2450 + let primaryChild: Fiber | null = null;
2451 + const areSuspenseChildrenConditionallyWrapped =
2452 + OffscreenComponent === -1;
2453 + if (areSuspenseChildrenConditionallyWrapped) {
2454 + primaryChild = fiber.child;
2455 + } else if (fiber.child !== null) {
2456 + primaryChild = fiber.child.child;
2457 + }
2458 + if (primaryChild !== null) {
2459 + mountChildrenRecursively(
2460 + primaryChild,
2461 + traceNearestHostComponentUpdate,
2462 + );
2463 + }
2464 }
2465 } else {
2345 - let primaryChild: Fiber | null = null;
2346 - const areSuspenseChildrenConditionallyWrapped =
2347 - OffscreenComponent === -1;
2348 - if (areSuspenseChildrenConditionallyWrapped) {
2349 - primaryChild = fiber.child;
2350 - } else if (fiber.child !== null) {
2351 - primaryChild = fiber.child.child;
2352 - }
2353 - if (primaryChild !== null) {
2466 + if (fiber.child !== null) {
2467 mountChildrenRecursively(
2355 - primaryChild,
2356 - newParentInstance,
2468 + fiber.child,
2469 traceNearestHostComponentUpdate,
2470 );
2471 }
2472 }
2361 - } else {
2362 - if (fiber.child !== null) {
2363 - mountChildrenRecursively(
2364 - fiber.child,
2365 - newParentInstance,
2366 - traceNearestHostComponentUpdate,
2367 - );
2473 + } finally {
2474 + if (shouldIncludeInTree) {
2475 + reconcilingParent = stashedParent;
2476 + previouslyReconciledSibling = stashedPrevious;
2477 + remainingReconcilingChildren = stashedRemaining;
2478 }
2479 }
2480
@@ -2380,26 +2490,54 @@ export function attach(
2490 debug('unmountFiberRecursively()', fiber, null);
2491 }
2492
2383 - // We might meet a nested Suspense on our way.
2384 - const isTimedOutSuspense =
2385 - fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
2493 + let fiberInstance = null;
2494
2387 - if (fiber.tag === HostHoistable) {
2388 - releaseHostResource(fiber, fiber.memoizedState);
2495 + const shouldIncludeInTree = !shouldFilterFiber(fiber);
2496 + const stashedParent = reconcilingParent;
2497 + const stashedPrevious = previouslyReconciledSibling;
2498 + const stashedRemaining = remainingReconcilingChildren;
2499 + if (shouldIncludeInTree) {
2500 + fiberInstance = getFiberInstanceThrows(fiber);
2501 + // Push a new DevTools instance parent while reconciling this subtree.
2502 + reconcilingParent = fiberInstance;
2503 + previouslyReconciledSibling = null;
2504 + // Move all the children of this instance to the remaining set.
2505 + // We'll move them back one by one, and anything that remains is deleted.
2506 + remainingReconcilingChildren = fiberInstance.firstChild;
2507 + fiberInstance.firstChild = null;
2508 }
2509 + try {
2510 + // We might meet a nested Suspense on our way.
2511 + const isTimedOutSuspense =
2512 + fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
2513
2391 - let child = fiber.child;
2392 - if (isTimedOutSuspense) {
2393 - // If it's showing fallback tree, let's traverse it instead.
2394 - const primaryChildFragment = fiber.child;
2395 - const fallbackChildFragment = primaryChildFragment
2396 - ? primaryChildFragment.sibling
2397 - : null;
2398 - // Skip over to the real Fiber child.
2399 - child = fallbackChildFragment ? fallbackChildFragment.child : null;
2400 - }
2514 + if (fiber.tag === HostHoistable) {
2515 + releaseHostResource(fiber, fiber.memoizedState);
2516 + }
2517 +
2518 + let child = fiber.child;
2519 + if (isTimedOutSuspense) {
2520 + // If it's showing fallback tree, let's traverse it instead.
2521 + const primaryChildFragment = fiber.child;
2522 + const fallbackChildFragment = primaryChildFragment
2523 + ? primaryChildFragment.sibling
2524 + : null;
2525 + // Skip over to the real Fiber child.
2526 + child = fallbackChildFragment ? fallbackChildFragment.child : null;
2527 + }
2528
2402 - unmountChildrenRecursively(child);
2529 + unmountChildrenRecursively(child);
2530 + } finally {
2531 + if (shouldIncludeInTree) {
2532 + reconcilingParent = stashedParent;
2533 + previouslyReconciledSibling = stashedPrevious;
2534 + remainingReconcilingChildren = stashedRemaining;
2535 + }
2536 + }
2537 + if (fiberInstance !== null) {
2538 + recordUnmount(fiber, true);
2539 + removeChild(fiberInstance);
2540 + }
2541 }
2542
2543 function unmountChildrenRecursively(firstChild: null | Fiber) {
@@ -2409,7 +2547,6 @@ export function attach(
2547 // We skip nodes without return because those are real unmounts.
2548 if (child.return !== null) {
2549 unmountFiberRecursively(child);
2412 - recordUnmount(child, true);
2550 }
2551 child = child.sibling;
2552 }
@@ -2551,7 +2688,6 @@ export function attach(
2688 function updateChildrenRecursively(
2689 nextFirstChild: null | Fiber,
2690 prevFirstChild: null | Fiber,
2554 - parentInstance: DevToolsInstance | null,
2691 traceNearestHostComponentUpdate: boolean,
2692 ): boolean {
2693 let shouldResetChildren = false;
@@ -2564,13 +2700,24 @@ export function attach(
2700 // they are either new mounts or alternates of previous children.
2701 // Schedule updates and mounts depending on whether alternates exist.
2702 // We don't track deletions here because they are reported separately.
2567 - if (nextChild.alternate) {
2703 + if (prevChildAtSameIndex === nextChild) {
2704 + // This set is unchanged. We're just going through it to place all the
2705 + // children again.
2706 + if (
2707 + updateFiberRecursively(
2708 + nextChild,
2709 + nextChild,
2710 + traceNearestHostComponentUpdate,
2711 + )
2712 + ) {
2713 + throw new Error('Updating the same fiber should not cause reorder');
2714 + }
2715 + } else if (nextChild.alternate) {
2716 const prevChild = nextChild.alternate;
2717 if (
2718 updateFiberRecursively(
2719 nextChild,
2720 prevChild,
2573 - parentInstance,
2721 traceNearestHostComponentUpdate,
2722 )
2723 ) {
@@ -2586,11 +2733,7 @@ export function attach(
2733 shouldResetChildren = true;
2734 }
2735 } else {
2589 - mountFiberRecursively(
2590 - nextChild,
2591 - parentInstance,
2592 - traceNearestHostComponentUpdate,
2593 - );
2736 + mountFiberRecursively(nextChild, traceNearestHostComponentUpdate);
2737 shouldResetChildren = true;
2738 }
2739 // Try the next child.
@@ -2612,7 +2755,6 @@ export function attach(
2755 function updateFiberRecursively(
2756 nextFiber: Fiber,
2757 prevFiber: Fiber,
2615 - parentInstance: DevToolsInstance | null,
2758 traceNearestHostComponentUpdate: boolean,
2759 ): boolean {
2760 // TODO: Do we really need to give this an instance eagerly if it's filtered?
@@ -2620,7 +2762,7 @@ export function attach(
2762 const id = fiberInstance.id;
2763
2764 if (__DEBUG__) {
2623 - debug('updateFiberRecursively()', nextFiber, parentInstance);
2765 + debug('updateFiberRecursively()', nextFiber, reconcilingParent);
2766 }
2767
2768 if (traceUpdatesEnabled) {
@@ -2659,157 +2801,193 @@ export function attach(
2801 }
2802
2803 const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
2662 - const newParentInstance = shouldIncludeInTree
2663 - ? fiberInstance
2664 - : parentInstance;
2665 -
2666 - if (nextFiber.tag === HostHoistable) {
2667 - releaseHostResource(prevFiber, prevFiber.memoizedState);
2668 - aquireHostResource(nextFiber, nextFiber.memoizedState);
2804 + if (shouldIncludeInTree) {
2805 + // Update the Fiber so we that we always keep the current Fiber on the data.
2806 + fiberInstance.data = nextFiber;
2807 + moveChild(fiberInstance);
2808 }
2809 + const stashedParent = reconcilingParent;
2810 + const stashedPrevious = previouslyReconciledSibling;
2811 + const stashedRemaining = remainingReconcilingChildren;
2812 + if (shouldIncludeInTree) {
2813 + // Push a new DevTools instance parent while reconciling this subtree.
2814 + reconcilingParent = fiberInstance;
2815 + previouslyReconciledSibling = null;
2816 + // Move all the children of this instance to the remaining set.
2817 + // We'll move them back one by one, and anything that remains is deleted.
2818 + remainingReconcilingChildren = fiberInstance.firstChild;
2819 + fiberInstance.firstChild = null;
2820 + }
2821 + try {
2822 + if (nextFiber.tag === HostHoistable) {
2823 + releaseHostResource(prevFiber, prevFiber.memoizedState);
2824 + aquireHostResource(nextFiber, nextFiber.memoizedState);
2825 + }
2826
2671 - const isSuspense = nextFiber.tag === SuspenseComponent;
2672 - let shouldResetChildren = false;
2673 - // The behavior of timed-out Suspense trees is unique.
2674 - // Rather than unmount the timed out content (and possibly lose important state),
2675 - // React re-parents this content within a hidden Fragment while the fallback is showing.
2676 - // This behavior doesn't need to be observable in the DevTools though.
2677 - // It might even result in a bad user experience for e.g. node selection in the Elements panel.
2678 - // The easiest fix is to strip out the intermediate Fragment fibers,
2679 - // so the Elements panel and Profiler don't need to special case them.
2680 - // Suspense components only have a non-null memoizedState if they're timed-out.
2681 - const prevDidTimeout = isSuspense && prevFiber.memoizedState !== null;
2682 - const nextDidTimeOut = isSuspense && nextFiber.memoizedState !== null;
2683 - // The logic below is inspired by the code paths in updateSuspenseComponent()
2684 - // inside ReactFiberBeginWork in the React source code.
2685 - if (prevDidTimeout && nextDidTimeOut) {
2686 - // Fallback -> Fallback:
2687 - // 1. Reconcile fallback set.
2688 - const nextFiberChild = nextFiber.child;
2689 - const nextFallbackChildSet = nextFiberChild
2690 - ? nextFiberChild.sibling
2691 - : null;
2692 - // Note: We can't use nextFiber.child.sibling.alternate
2693 - // because the set is special and alternate may not exist.
2694 - const prevFiberChild = prevFiber.child;
2695 - const prevFallbackChildSet = prevFiberChild
2696 - ? prevFiberChild.sibling
2697 - : null;
2827 + const isSuspense = nextFiber.tag === SuspenseComponent;
2828 + let shouldResetChildren = false;
2829 + // The behavior of timed-out Suspense trees is unique.
2830 + // Rather than unmount the timed out content (and possibly lose important state),
2831 + // React re-parents this content within a hidden Fragment while the fallback is showing.
2832 + // This behavior doesn't need to be observable in the DevTools though.
2833 + // It might even result in a bad user experience for e.g. node selection in the Elements panel.
2834 + // The easiest fix is to strip out the intermediate Fragment fibers,
2835 + // so the Elements panel and Profiler don't need to special case them.
2836 + // Suspense components only have a non-null memoizedState if they're timed-out.
2837 + const prevDidTimeout = isSuspense && prevFiber.memoizedState !== null;
2838 + const nextDidTimeOut = isSuspense && nextFiber.memoizedState !== null;
2839 + // The logic below is inspired by the code paths in updateSuspenseComponent()
2840 + // inside ReactFiberBeginWork in the React source code.
2841 + if (prevDidTimeout && nextDidTimeOut) {
2842 + // Fallback -> Fallback:
2843 + // 1. Reconcile fallback set.
2844 + const nextFiberChild = nextFiber.child;
2845 + const nextFallbackChildSet = nextFiberChild
2846 + ? nextFiberChild.sibling
2847 + : null;
2848 + // Note: We can't use nextFiber.child.sibling.alternate
2849 + // because the set is special and alternate may not exist.
2850 + const prevFiberChild = prevFiber.child;
2851 + const prevFallbackChildSet = prevFiberChild
2852 + ? prevFiberChild.sibling
2853 + : null;
2854
2699 - if (prevFallbackChildSet == null && nextFallbackChildSet != null) {
2700 - mountChildrenRecursively(
2701 - nextFallbackChildSet,
2702 - newParentInstance,
2703 - traceNearestHostComponentUpdate,
2704 - );
2855 + if (prevFallbackChildSet == null && nextFallbackChildSet != null) {
2856 + mountChildrenRecursively(
2857 + nextFallbackChildSet,
2858 + traceNearestHostComponentUpdate,
2859 + );
2860
2706 - shouldResetChildren = true;
2707 - }
2861 + shouldResetChildren = true;
2862 + }
2863
2709 - if (
2710 - nextFallbackChildSet != null &&
2711 - prevFallbackChildSet != null &&
2712 - updateFiberRecursively(
2713 - nextFallbackChildSet,
2714 - prevFallbackChildSet,
2715 - newParentInstance,
2716 - traceNearestHostComponentUpdate,
2717 - )
2718 - ) {
2719 - shouldResetChildren = true;
2720 - }
2721 - } else if (prevDidTimeout && !nextDidTimeOut) {
2722 - // Fallback -> Primary:
2723 - // 1. Unmount fallback set
2724 - // Note: don't emulate fallback unmount because React actually did it.
2725 - // 2. Mount primary set
2726 - const nextPrimaryChildSet = nextFiber.child;
2727 - if (nextPrimaryChildSet !== null) {
2728 - mountChildrenRecursively(
2729 - nextPrimaryChildSet,
2730 - newParentInstance,
2731 - traceNearestHostComponentUpdate,
2732 - );
2733 - }
2734 - shouldResetChildren = true;
2735 - } else if (!prevDidTimeout && nextDidTimeOut) {
2736 - // Primary -> Fallback:
2737 - // 1. Hide primary set
2738 - // This is not a real unmount, so it won't get reported by React.
2739 - // We need to manually walk the previous tree and record unmounts.
2740 - unmountFiberRecursively(prevFiber);
2741 - // 2. Mount fallback set
2742 - const nextFiberChild = nextFiber.child;
2743 - const nextFallbackChildSet = nextFiberChild
2744 - ? nextFiberChild.sibling
2745 - : null;
2746 - if (nextFallbackChildSet != null) {
2747 - mountChildrenRecursively(
2748 - nextFallbackChildSet,
2749 - newParentInstance,
2750 - traceNearestHostComponentUpdate,
2751 - );
2752 - shouldResetChildren = true;
2753 - }
2754 - } else {
2755 - // Common case: Primary -> Primary.
2756 - // This is the same code path as for non-Suspense fibers.
2757 - if (nextFiber.child !== prevFiber.child) {
2864 if (
2759 - updateChildrenRecursively(
2760 - nextFiber.child,
2761 - prevFiber.child,
2762 - newParentInstance,
2865 + nextFallbackChildSet != null &&
2866 + prevFallbackChildSet != null &&
2867 + updateFiberRecursively(
2868 + nextFallbackChildSet,
2869 + prevFallbackChildSet,
2870 traceNearestHostComponentUpdate,
2871 )
2872 ) {
2873 shouldResetChildren = true;
2874 }
2875 + } else if (prevDidTimeout && !nextDidTimeOut) {
2876 + // Fallback -> Primary:
2877 + // 1. Unmount fallback set
2878 + // Note: don't emulate fallback unmount because React actually did it.
2879 + // 2. Mount primary set
2880 + const nextPrimaryChildSet = nextFiber.child;
2881 + if (nextPrimaryChildSet !== null) {
2882 + mountChildrenRecursively(
2883 + nextPrimaryChildSet,
2884 + traceNearestHostComponentUpdate,
2885 + );
2886 + }
2887 + shouldResetChildren = true;
2888 + } else if (!prevDidTimeout && nextDidTimeOut) {
2889 + // Primary -> Fallback:
2890 + // 1. Hide primary set
2891 + // This is not a real unmount, so it won't get reported by React.
2892 + // We need to manually walk the previous tree and record unmounts.
2893 + unmountChildrenRecursively(prevFiber.child);
2894 + // 2. Mount fallback set
2895 + const nextFiberChild = nextFiber.child;
2896 + const nextFallbackChildSet = nextFiberChild
2897 + ? nextFiberChild.sibling
2898 + : null;
2899 + if (nextFallbackChildSet != null) {
2900 + mountChildrenRecursively(
2901 + nextFallbackChildSet,
2902 + traceNearestHostComponentUpdate,
2903 + );
2904 + shouldResetChildren = true;
2905 + }
2906 } else {
2769 - if (traceUpdatesEnabled) {
2770 - // If we're tracing updates and we've bailed out before reaching a host node,
2771 - // we should fall back to recursively marking the nearest host descendants for highlight.
2772 - if (traceNearestHostComponentUpdate) {
2773 - const hostInstances = findAllCurrentHostInstances(
2774 - getFiberInstanceThrows(nextFiber),
2775 - );
2776 - hostInstances.forEach(hostInstance => {
2777 - traceUpdatesForNodes.add(hostInstance);
2778 - });
2907 + // Common case: Primary -> Primary.
2908 + // This is the same code path as for non-Suspense fibers.
2909 + if (nextFiber.child !== prevFiber.child) {
2910 + if (
2911 + updateChildrenRecursively(
2912 + nextFiber.child,
2913 + prevFiber.child,
2914 + traceNearestHostComponentUpdate,
2915 + )
2916 + ) {
2917 + shouldResetChildren = true;
2918 + }
2919 + } else {
2920 + // Children are unchanged.
2921 + if (shouldIncludeInTree) {
2922 + // All the remaining children will be children of this same fiber so we can just reuse them.
2923 + // I.e. we just restore them by undoing what we did above.
2924 + fiberInstance.firstChild = remainingReconcilingChildren;
2925 + } else {
2926 + // If this fiber is filtered there might be changes to this set elsewhere so we have
2927 + // to visit each child to place it back in the set. We let the child bail out instead.
2928 + if (
2929 + updateChildrenRecursively(nextFiber.child, prevFiber.child, false)
2930 + ) {
2931 + throw new Error(
2932 + 'The children should not have changed if we pass in the same set.',
2933 + );
2934 + }
2935 + }
2936 +
2937 + if (traceUpdatesEnabled) {
2938 + // If we're tracing updates and we've bailed out before reaching a host node,
2939 + // we should fall back to recursively marking the nearest host descendants for highlight.
2940 + if (traceNearestHostComponentUpdate) {
2941 + const hostInstances = findAllCurrentHostInstances(
2942 + getFiberInstanceThrows(nextFiber),
2943 + );
2944 + hostInstances.forEach(hostInstance => {
2945 + traceUpdatesForNodes.add(hostInstance);
2946 + });
2947 + }
2948 }
2949 }
2950 }
2782 - }
2951
2784 - if (shouldIncludeInTree) {
2785 - const isProfilingSupported = nextFiber.hasOwnProperty('treeBaseDuration');
2786 - if (isProfilingSupported) {
2787 - recordProfilingDurations(nextFiber);
2788 - }
2789 - }
2790 - if (shouldResetChildren) {
2791 - // We need to crawl the subtree for closest non-filtered Fibers
2792 - // so that we can display them in a flat children set.
2952 if (shouldIncludeInTree) {
2794 - // Normally, search for children from the rendered child.
2795 - let nextChildSet = nextFiber.child;
2796 - if (nextDidTimeOut) {
2797 - // Special case: timed-out Suspense renders the fallback set.
2798 - const nextFiberChild = nextFiber.child;
2799 - nextChildSet = nextFiberChild ? nextFiberChild.sibling : null;
2953 + const isProfilingSupported =
2954 + nextFiber.hasOwnProperty('treeBaseDuration');
2955 + if (isProfilingSupported) {
2956 + recordProfilingDurations(nextFiber);
2957 }
2801 - if (nextChildSet != null) {
2802 - recordResetChildren(fiberInstance, nextChildSet);
2958 + }
2959 + if (shouldResetChildren) {
2960 + // We need to crawl the subtree for closest non-filtered Fibers
2961 + // so that we can display them in a flat children set.
2962 + if (shouldIncludeInTree) {
2963 + // Normally, search for children from the rendered child.
2964 + let nextChildSet = nextFiber.child;
2965 + if (nextDidTimeOut) {
2966 + // Special case: timed-out Suspense renders the fallback set.
2967 + const nextFiberChild = nextFiber.child;
2968 + nextChildSet = nextFiberChild ? nextFiberChild.sibling : null;
2969 + }
2970 + if (nextChildSet != null) {
2971 + if (reconcilingParent !== null) {
2972 + recordResetChildren(reconcilingParent, nextChildSet);
2973 + }
2974 + }
2975 + // We've handled the child order change for this Fiber.
2976 + // Since it's included, there's no need to invalidate parent child order.
2977 + return false;
2978 + } else {
2979 + // Let the closest unfiltered parent Fiber reset its child order instead.
2980 + return true;
2981 }
2804 - // We've handled the child order change for this Fiber.
2805 - // Since it's included, there's no need to invalidate parent child order.
2806 - return false;
2982 } else {
2808 - // Let the closest unfiltered parent Fiber reset its child order instead.
2809 - return true;
2983 + return false;
2984 + }
2985 + } finally {
2986 + if (shouldIncludeInTree) {
2987 + reconcilingParent = stashedParent;
2988 + previouslyReconciledSibling = stashedPrevious;
2989 + remainingReconcilingChildren = stashedRemaining;
2990 }
2811 - } else {
2812 - return false;
2991 }
2992 }
2993
@@ -2874,7 +3052,7 @@ export function attach(
3052 };
3053 }
3054
2877 - mountFiberRecursively(root.current, null, false);
3055 + mountFiberRecursively(root.current, false);
3056 flushPendingEvents(root);
3057 currentRootID = -1;
3058 });
@@ -2973,10 +3151,10 @@ export function attach(
3151 if (!wasMounted && isMounted) {
3152 // Mount a new root.
3153 setRootPseudoKey(currentRootID, current);
2976 - mountFiberRecursively(current, null, false);
3154 + mountFiberRecursively(current, false);
3155 } else if (wasMounted && isMounted) {
3156 // Update an existing root.
2979 - updateFiberRecursively(current, alternate, null, false);
3157 + updateFiberRecursively(current, alternate, false);
3158 } else if (wasMounted && !isMounted) {
3159 // Unmount an existing root.
3160 removeRootPseudoKey(currentRootID);
@@ -2985,7 +3163,7 @@ export function attach(
3163 } else {
3164 // Mount a new root.
3165 setRootPseudoKey(currentRootID, current);
2988 - mountFiberRecursively(current, null, false);
3166 + mountFiberRecursively(current, false);
3167 }
3168
3169 if (isProfiling && isProfilingSupported) {