[DevTools] Track virtual instances on the tracked path for selections (#30802)
This appends a (filtered) virtual instance path at the end of the fiber path. If a virtual instance is selected inside the fiber. The main part of the path is still just the fiber path since that's the semantically stateful part. Then we just tack on a few virtual path frames at the end if we're currently selecting a specific Server Component within the nearest Fiber. I also took the opportunity to fix a bug which caused selections inside Suspense boundaries to not be tracked.
Sebastian Markbåge committed
Aug 29, 2024 at 12:45 UTC
e33a7233a76e1164bd1a9c4b8115abb575b48c50
1 file changed
+121
-39
packages/react-devtools-shared/src/backend/fiber/renderer.js
+121
-39
@@ -2266,16 +2266,11 @@ export function attach(
2266
debug('recordUnmount()', fiber, null);
2267
}
2268
2269
- if (trackedPathMatchFiber !== null) {
2269
+ if (trackedPathMatchInstance === fiberInstance) {
2270
// We're in the process of trying to restore previous selection.
2271
// If this fiber matched but is being unmounted, there's no use trying.
2272
// Reset the state so we don't keep holding onto it.
2273
- if (
2274
- fiber === trackedPathMatchFiber ||
2275
- fiber === trackedPathMatchFiber.alternate
2276
- ) {
2277
- setTrackedPath(null);
2278
- }
2273
+ setTrackedPath(null);
2274
}
2275
2276
const id = fiberInstance.id;
@@ -2386,6 +2381,14 @@ export function attach(
2381
traceNearestHostComponentUpdate: boolean,
2382
virtualLevel: number, // the nth level of virtual instances
2383
): void {
2384
+ // If we have the tree selection from previous reload, try to match this Instance.
2385
+ // Also remember whether to do the same for siblings.
2386
+ const mightSiblingsBeOnTrackedPath =
2387
+ updateVirtualTrackedPathStateBeforeMount(
2388
+ virtualInstance,
2389
+ reconcilingParent,
2390
+ );
2391
+
2392
const stashedParent = reconcilingParent;
2393
const stashedPrevious = previouslyReconciledSibling;
2394
const stashedRemaining = remainingReconcilingChildren;
@@ -2406,13 +2409,16 @@ export function attach(
2409
reconcilingParent = stashedParent;
2410
previouslyReconciledSibling = stashedPrevious;
2411
remainingReconcilingChildren = stashedRemaining;
2412
+ updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath);
2413
}
2414
}
2415
2416
function recordVirtualUnmount(instance: VirtualInstance) {
2413
- if (trackedPathMatchFiber !== null) {
2417
+ if (trackedPathMatchInstance === instance) {
2418
// We're in the process of trying to restore previous selection.
2415
- // TODO: Handle virtual instances on the tracked path.
2419
+ // If this fiber matched but is being unmounted, there's no use trying.
2420
+ // Reset the state so we don't keep holding onto it.
2421
+ setTrackedPath(null);
2422
}
2423
2424
const id = instance.id;
@@ -2521,17 +2527,20 @@ export function attach(
2527
debug('mountFiberRecursively()', fiber, reconcilingParent);
2528
}
2529
2524
- // If we have the tree selection from previous reload, try to match this Fiber.
2525
- // Also remember whether to do the same for siblings.
2526
- const mightSiblingsBeOnTrackedPath =
2527
- updateTrackedPathStateBeforeMount(fiber);
2528
-
2530
const shouldIncludeInTree = !shouldFilterFiber(fiber);
2531
let newInstance = null;
2532
if (shouldIncludeInTree) {
2533
newInstance = recordMount(fiber, reconcilingParent);
2534
insertChild(newInstance);
2535
}
2536
+
2537
+ // If we have the tree selection from previous reload, try to match this Fiber.
2538
+ // Also remember whether to do the same for siblings.
2539
+ const mightSiblingsBeOnTrackedPath = updateTrackedPathStateBeforeMount(
2540
+ fiber,
2541
+ newInstance,
2542
+ );
2543
+
2544
const stashedParent = reconcilingParent;
2545
const stashedPrevious = previouslyReconciledSibling;
2546
const stashedRemaining = remainingReconcilingChildren;
@@ -2570,14 +2579,15 @@ export function attach(
2579
const fallbackChildFragment = primaryChildFragment
2580
? primaryChildFragment.sibling
2581
: null;
2573
- const fallbackChild = fallbackChildFragment
2574
- ? fallbackChildFragment.child
2575
- : null;
2576
- if (fallbackChild !== null) {
2577
- mountChildrenRecursively(
2578
- fallbackChild,
2579
- traceNearestHostComponentUpdate,
2580
- );
2582
+ if (fallbackChildFragment) {
2583
+ const fallbackChild = fallbackChildFragment.child;
2584
+ if (fallbackChild !== null) {
2585
+ updateTrackedPathStateBeforeMount(fallbackChildFragment, null);
2586
+ mountChildrenRecursively(
2587
+ fallbackChild,
2588
+ traceNearestHostComponentUpdate,
2589
+ );
2590
+ }
2591
}
2592
} else {
2593
let primaryChild: Fiber | null = null;
@@ -2587,6 +2597,7 @@ export function attach(
2597
primaryChild = fiber.child;
2598
} else if (fiber.child !== null) {
2599
primaryChild = fiber.child.child;
2600
+ updateTrackedPathStateBeforeMount(fiber.child, null);
2601
}
2602
if (primaryChild !== null) {
2603
mountChildrenRecursively(
@@ -5262,13 +5273,15 @@ export function attach(
5273
// Remember if we're trying to restore the selection after reload.
5274
// In that case, we'll do some extra checks for matching mounts.
5275
let trackedPath: Array<PathFrame> | null = null;
5265
- let trackedPathMatchFiber: Fiber | null = null;
5276
+ let trackedPathMatchFiber: Fiber | null = null; // This is the deepest unfiltered match of a Fiber.
5277
+ let trackedPathMatchInstance: DevToolsInstance | null = null; // This is the deepest matched filtered Instance.
5278
let trackedPathMatchDepth = -1;
5279
let mightBeOnTrackedPath = false;
5280
5281
function setTrackedPath(path: Array<PathFrame> | null) {
5282
if (path === null) {
5283
trackedPathMatchFiber = null;
5284
+ trackedPathMatchInstance = null;
5285
trackedPathMatchDepth = -1;
5286
mightBeOnTrackedPath = false;
5287
}
@@ -5278,7 +5291,10 @@ export function attach(
5291
// We call this before traversing a new mount.
5292
// It remembers whether this Fiber is the next best match for tracked path.
5293
// The return value signals whether we should keep matching siblings or not.
5281
- function updateTrackedPathStateBeforeMount(fiber: Fiber): boolean {
5294
+ function updateTrackedPathStateBeforeMount(
5295
+ fiber: Fiber,
5296
+ fiberInstance: null | FiberInstance,
5297
+ ): boolean {
5298
if (trackedPath === null || !mightBeOnTrackedPath) {
5299
// Fast path: there's nothing to track so do nothing and ignore siblings.
5300
return false;
@@ -5306,6 +5322,9 @@ export function attach(
5322
) {
5323
// We have our next match.
5324
trackedPathMatchFiber = fiber;
5325
+ if (fiberInstance !== null) {
5326
+ trackedPathMatchInstance = fiberInstance;
5327
+ }
5328
trackedPathMatchDepth++;
5329
// Are we out of frames to match?
5330
// $FlowFixMe[incompatible-use] found when upgrading Flow
@@ -5322,6 +5341,11 @@ export function attach(
5341
return false;
5342
}
5343
}
5344
+ if (trackedPathMatchFiber === null && fiberInstance === null) {
5345
+ // We're now looking for a Virtual Instance. It might be inside filtered Fibers
5346
+ // so we keep looking below.
5347
+ return true;
5348
+ }
5349
// This Fiber's parent is on the path, but this Fiber itself isn't.
5350
// There's no need to check its children--they won't be on the path either.
5351
mightBeOnTrackedPath = false;
@@ -5329,6 +5353,57 @@ export function attach(
5353
return true;
5354
}
5355
5356
+ function updateVirtualTrackedPathStateBeforeMount(
5357
+ virtualInstance: VirtualInstance,
5358
+ parentInstance: null | DevToolsInstance,
5359
+ ): boolean {
5360
+ if (trackedPath === null || !mightBeOnTrackedPath) {
5361
+ // Fast path: there's nothing to track so do nothing and ignore siblings.
5362
+ return false;
5363
+ }
5364
+ // Check if we've matched our nearest unfiltered parent so far.
5365
+ if (trackedPathMatchInstance === parentInstance) {
5366
+ const actualFrame = getVirtualPathFrame(virtualInstance);
5367
+ // $FlowFixMe[incompatible-use] found when upgrading Flow
5368
+ const expectedFrame = trackedPath[trackedPathMatchDepth + 1];
5369
+ if (expectedFrame === undefined) {
5370
+ throw new Error('Expected to see a frame at the next depth.');
5371
+ }
5372
+ if (
5373
+ actualFrame.index === expectedFrame.index &&
5374
+ actualFrame.key === expectedFrame.key &&
5375
+ actualFrame.displayName === expectedFrame.displayName
5376
+ ) {
5377
+ // We have our next match.
5378
+ trackedPathMatchFiber = null; // Don't bother looking in Fibers anymore. We're deeper now.
5379
+ trackedPathMatchInstance = virtualInstance;
5380
+ trackedPathMatchDepth++;
5381
+ // Are we out of frames to match?
5382
+ // $FlowFixMe[incompatible-use] found when upgrading Flow
5383
+ if (trackedPathMatchDepth === trackedPath.length - 1) {
5384
+ // There's nothing that can possibly match afterwards.
5385
+ // Don't check the children.
5386
+ mightBeOnTrackedPath = false;
5387
+ } else {
5388
+ // Check the children, as they might reveal the next match.
5389
+ mightBeOnTrackedPath = true;
5390
+ }
5391
+ // In either case, since we have a match, we don't need
5392
+ // to check the siblings. They'll never match.
5393
+ return false;
5394
+ }
5395
+ }
5396
+ if (trackedPathMatchFiber !== null) {
5397
+ // We're still looking for a Fiber which might be underneath this instance.
5398
+ return true;
5399
+ }
5400
+ // This Instance's parent is on the path, but this Instance itself isn't.
5401
+ // There's no need to check its children--they won't be on the path either.
5402
+ mightBeOnTrackedPath = false;
5403
+ // However, one of its siblings may be on the path so keep searching.
5404
+ return true;
5405
+ }
5406
+
5407
function updateTrackedPathStateAfterMount(
5408
mightSiblingsBeOnTrackedPath: boolean,
5409
) {
@@ -5428,6 +5503,14 @@ export function attach(
5503
};
5504
}
5505
5506
+ function getVirtualPathFrame(virtualInstance: VirtualInstance): PathFrame {
5507
+ return {
5508
+ displayName: virtualInstance.data.name || '',
5509
+ key: virtualInstance.data.key == null ? null : virtualInstance.data.key,
5510
+ index: -1, // We use -1 to indicate that this is a virtual path frame.
5511
+ };
5512
+ }
5513
+
5514
// Produces a serializable representation that does a best effort
5515
// of identifying a particular Fiber between page reloads.
5516
// The return path will contain Fibers that are "invisible" to the store
@@ -5437,13 +5520,20 @@ export function attach(
5520
if (devtoolsInstance === undefined) {
5521
return null;
5522
}
5440
- if (devtoolsInstance.kind !== FIBER_INSTANCE) {
5441
- // TODO: Handle VirtualInstance.
5442
- return null;
5443
- }
5523
5445
- let fiber: null | Fiber = devtoolsInstance.data;
5524
const keyPath = [];
5525
+
5526
+ let inst: DevToolsInstance = devtoolsInstance;
5527
+ while (inst.kind === VIRTUAL_INSTANCE) {
5528
+ keyPath.push(getVirtualPathFrame(inst));
5529
+ if (inst.parent === null) {
5530
+ // This is a bug but non-essential. We should've found a root instance.
5531
+ return null;
5532
+ }
5533
+ inst = inst.parent;
5534
+ }
5535
+
5536
+ let fiber: null | Fiber = inst.data;
5537
while (fiber !== null) {
5538
// $FlowFixMe[incompatible-call] found when upgrading Flow
5539
keyPath.push(getPathFrame(fiber));
@@ -5459,20 +5549,12 @@ export function attach(
5549
// Nothing to match.
5550
return null;
5551
}
5462
- if (trackedPathMatchFiber === null) {
5552
+ if (trackedPathMatchInstance === null) {
5553
// We didn't find anything.
5554
return null;
5555
}
5466
- // Find the closest Fiber store is aware of.
5467
- let fiber: null | Fiber = trackedPathMatchFiber;
5468
- while (fiber !== null && shouldFilterFiber(fiber)) {
5469
- fiber = fiber.return;
5470
- }
5471
- if (fiber === null) {
5472
- return null;
5473
- }
5556
return {
5475
- id: getFiberIDThrows(fiber),
5557
+ id: trackedPathMatchInstance.id,
5558
// $FlowFixMe[incompatible-use] found when upgrading Flow
5559
isFullMatch: trackedPathMatchDepth === trackedPath.length - 1,
5560
};