@samitouri / QOS-React / commits / 49496d4937

[DevTools] Support Server Components in Tree (#30684)

This adds VirtualInstances to the tree. Each Fiber has a list of its parent Server Components in `_debugInfo`. The algorithm is that when we enter a set of fibers, we actually traverse level 0 of all the `_debugInfo` in each fiber. Then level 1 of each `_debugInfo` and so on. It would be simpler if `_debugInfo` only contained Server Component since then we could just look at the index in the array but it actually contains other data as well which leads to multiple passes but we don't expect it to have a lot of levels before hitting a reified fiber. Finally when we hit the end a traverse the fiber itself. This lets us match consecutive `ReactComponentInfo` that are all the same at the same level. This creates a single VirtualInstance for each sequence. This lets the same Server Component instance that's a parent to multiple children appear as a single Instance instead of one per Fiber. Since a Server Component's result can be rendered in more than one place there's not a 1:1 mapping though. If it is in different parents or if the sequence is interrupted, then it gets split into two different instances with the same `ReactComponentInfo` data. The real interesting case is what happens during updates because this algorithm means that a Fiber can become reparented during an update to end up in a different VirtualInstance. The ideal would maybe be that the frontend could deal with this reparenting but instead I basically just unmount the previous instance (and its children) and mount a new instance which leads to some interesting scenarios. This is inline with the strategy I was intending to pursue anyway where instances are reconciled against the previous children of the same parent instead of the `fiberToFiberInstance` map - which would let us get rid of that map. In that case the model is resilient to Fiber being in more than one place at a time. However this unmount/remount does mean that we can lose selection when this happens. We could maybe do something like using the tracked path like I did for component filters. Ideally it's a weird edge case though because you'd typically not have it. The main case that it happens now is for reorders of list of server components. In that case basically all the children move between server components while the server components themselves stay in place. We should really include the key in server components so that we can reconcile them using the key to handle reorders which would solve the common case anyway. I convert the name to the `Env(Name)` pattern which allows the Environment Name to be used as a badge. <img width="1105" alt="Screenshot 2024-08-13 at 9 55 29 PM" src="https://github.com/user-attachments/assets/323c20ba-b655-4ee8-84fa-8233f55d2999"> (Screenshot is with #30667. I haven't tried it with the alternative fix.) --------- Co-authored-by: Ruslan Lesiutin <rdlesyutin@gmail.com>

Sebastian Markbåge committed Aug 14, 2024 at 11:16 UTC 49496d493797d4df1b9496f64a6103d9a7d23968
5 files changed +912 -86
packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
+31
@@ -3150,4 +3150,35 @@ describe('InspectedElement', () => {
3150 <Child> ⚠
3151 `);
3152 });
3153 +
3154 + // @reactVersion > 18.2
3155 + it('should inspect server components', async () => {
3156 + const ChildPromise = Promise.resolve(<div />);
3157 + ChildPromise._debugInfo = [
3158 + {
3159 + name: 'ServerComponent',
3160 + env: 'Server',
3161 + owner: null,
3162 + },
3163 + ];
3164 + const Parent = () => ChildPromise;
3165 +
3166 + await utils.actAsync(() => {
3167 + modernRender(<Parent />);
3168 + });
3169 +
3170 + const inspectedElement = await inspectElementAtIndex(1);
3171 + expect(inspectedElement).toMatchInlineSnapshot(`
3172 + {
3173 + "context": null,
3174 + "events": undefined,
3175 + "hooks": null,
3176 + "id": 3,
3177 + "owners": null,
3178 + "props": null,
3179 + "rootType": "createRoot()",
3180 + "state": null,
3181 + }
3182 + `);
3183 + });
3184 });
packages/react-devtools-shared/src/__tests__/store-test.js
+276
@@ -2212,4 +2212,280 @@ describe('Store', () => {
2212 `);
2213 });
2214 });
2215 +
2216 + // @reactVersion > 18.2
2217 + it('does not show server components without any children reified children', async () => {
2218 + // A Server Component that doesn't render into anything on the client doesn't show up.
2219 + const ServerPromise = Promise.resolve(null);
2220 + ServerPromise._debugInfo = [
2221 + {
2222 + name: 'ServerComponent',
2223 + env: 'Server',
2224 + owner: null,
2225 + },
2226 + ];
2227 + const App = () => ServerPromise;
2228 +
2229 + await actAsync(() => render(<App />));
2230 + expect(store).toMatchInlineSnapshot(`
2231 + [root]
2232 + <App>
2233 + `);
2234 + });
2235 +
2236 + // @reactVersion > 18.2
2237 + it('does show a server component that renders into a filtered node', async () => {
2238 + const ServerPromise = Promise.resolve(<div />);
2239 + ServerPromise._debugInfo = [
2240 + {
2241 + name: 'ServerComponent',
2242 + env: 'Server',
2243 + owner: null,
2244 + },
2245 + ];
2246 + const App = () => ServerPromise;
2247 +
2248 + await actAsync(() => render(<App />));
2249 + expect(store).toMatchInlineSnapshot(`
2250 + [root]
2251 + ▾ <App>
2252 + <ServerComponent> [Server]
2253 + `);
2254 + });
2255 +
2256 + it('can render the same server component twice', async () => {
2257 + function ClientComponent() {
2258 + return <div />;
2259 + }
2260 + const ServerPromise = Promise.resolve(<ClientComponent />);
2261 + ServerPromise._debugInfo = [
2262 + {
2263 + name: 'ServerComponent',
2264 + env: 'Server',
2265 + owner: null,
2266 + },
2267 + ];
2268 + const App = () => (
2269 + <>
2270 + {ServerPromise}
2271 + <ClientComponent />
2272 + {ServerPromise}
2273 + </>
2274 + );
2275 +
2276 + await actAsync(() => render(<App />));
2277 + expect(store).toMatchInlineSnapshot(`
2278 + [root]
2279 + ▾ <App>
2280 + ▾ <ServerComponent> [Server]
2281 + <ClientComponent>
2282 + <ClientComponent>
2283 + ▾ <ServerComponent> [Server]
2284 + <ClientComponent>
2285 + `);
2286 + });
2287 +
2288 + // @reactVersion > 18.2
2289 + it('collapses multiple parent server components into one', async () => {
2290 + function ClientComponent() {
2291 + return <div />;
2292 + }
2293 + const ServerPromise = Promise.resolve(<ClientComponent />);
2294 + ServerPromise._debugInfo = [
2295 + {
2296 + name: 'ServerComponent',
2297 + env: 'Server',
2298 + owner: null,
2299 + },
2300 + ];
2301 + const ServerPromise2 = Promise.resolve(<ClientComponent />);
2302 + ServerPromise2._debugInfo = [
2303 + {
2304 + name: 'ServerComponent2',
2305 + env: 'Server',
2306 + owner: null,
2307 + },
2308 + ];
2309 + const App = ({initial}) => (
2310 + <>
2311 + {ServerPromise}
2312 + {ServerPromise}
2313 + {ServerPromise2}
2314 + {initial ? null : ServerPromise2}
2315 + </>
2316 + );
2317 +
2318 + await actAsync(() => render(<App initial={true} />));
2319 + expect(store).toMatchInlineSnapshot(`
2320 + [root]
2321 + ▾ <App>
2322 + ▾ <ServerComponent> [Server]
2323 + <ClientComponent>
2324 + <ClientComponent>
2325 + ▾ <ServerComponent2> [Server]
2326 + <ClientComponent>
2327 + `);
2328 +
2329 + await actAsync(() => render(<App initial={false} />));
2330 + expect(store).toMatchInlineSnapshot(`
2331 + [root]
2332 + ▾ <App>
2333 + ▾ <ServerComponent> [Server]
2334 + <ClientComponent>
2335 + <ClientComponent>
2336 + ▾ <ServerComponent2> [Server]
2337 + <ClientComponent>
2338 + <ClientComponent>
2339 + `);
2340 + });
2341 +
2342 + // @reactVersion > 18.2
2343 + it('can reparent a child when the server components change', async () => {
2344 + function ClientComponent() {
2345 + return <div />;
2346 + }
2347 + const ServerPromise = Promise.resolve(<ClientComponent />);
2348 + ServerPromise._debugInfo = [
2349 + {
2350 + name: 'ServerAB',
2351 + env: 'Server',
2352 + owner: null,
2353 + },
2354 + ];
2355 + const ServerPromise2 = Promise.resolve(<ClientComponent />);
2356 + ServerPromise2._debugInfo = [
2357 + {
2358 + name: 'ServerA',
2359 + env: 'Server',
2360 + owner: null,
2361 + },
2362 + {
2363 + name: 'ServerB',
2364 + env: 'Server',
2365 + owner: null,
2366 + },
2367 + ];
2368 + const App = ({initial}) => (initial ? ServerPromise : ServerPromise2);
2369 +
2370 + await actAsync(() => render(<App initial={true} />));
2371 + expect(store).toMatchInlineSnapshot(`
2372 + [root]
2373 + ▾ <App>
2374 + ▾ <ServerAB> [Server]
2375 + <ClientComponent>
2376 + `);
2377 +
2378 + await actAsync(() => render(<App initial={false} />));
2379 + expect(store).toMatchInlineSnapshot(`
2380 + [root]
2381 + ▾ <App>
2382 + ▾ <ServerA> [Server]
2383 + ▾ <ServerB> [Server]
2384 + <ClientComponent>
2385 + `);
2386 + });
2387 +
2388 + // @reactVersion > 18.2
2389 + it('splits a server component parent when a different child appears between', async () => {
2390 + function ClientComponent() {
2391 + return <div />;
2392 + }
2393 + const ServerPromise = Promise.resolve(<ClientComponent />);
2394 + ServerPromise._debugInfo = [
2395 + {
2396 + name: 'ServerComponent',
2397 + env: 'Server',
2398 + owner: null,
2399 + },
2400 + ];
2401 + const App = ({initial}) =>
2402 + initial ? (
2403 + <>
2404 + {ServerPromise}
2405 + {null}
2406 + {ServerPromise}
2407 + </>
2408 + ) : (
2409 + <>
2410 + {ServerPromise}
2411 + <ClientComponent />
2412 + {ServerPromise}
2413 + </>
2414 + );
2415 +
2416 + await actAsync(() => render(<App initial={true} />));
2417 + // Initially the Server Component only appears once because the children
2418 + // are consecutive.
2419 + expect(store).toMatchInlineSnapshot(`
2420 + [root]
2421 + ▾ <App>
2422 + ▾ <ServerComponent> [Server]
2423 + <ClientComponent>
2424 + <ClientComponent>
2425 + `);
2426 +
2427 + // Later the same instance gets split into two when it is no longer
2428 + // consecutive so we need two virtual instances to represent two parents.
2429 + await actAsync(() => render(<App initial={false} />));
2430 + expect(store).toMatchInlineSnapshot(`
2431 + [root]
2432 + ▾ <App>
2433 + ▾ <ServerComponent> [Server]
2434 + <ClientComponent>
2435 + <ClientComponent>
2436 + ▾ <ServerComponent> [Server]
2437 + <ClientComponent>
2438 + `);
2439 + });
2440 +
2441 + // @reactVersion > 18.2
2442 + it('can reorder keyed components', async () => {
2443 + function ClientComponent({text}) {
2444 + return <div>{text}</div>;
2445 + }
2446 + function getServerComponent(key) {
2447 + const ServerPromise = Promise.resolve(
2448 + <ClientComponent key={key} text={key} />,
2449 + );
2450 + ServerPromise._debugInfo = [
2451 + {
2452 + name: 'ServerComponent',
2453 + env: 'Server',
2454 + owner: null,
2455 + // TODO: Ideally the debug info should include the "key" too to
2456 + // preserve the virtual identity of the server component when
2457 + // reordered. Atm only the children of it gets reparented.
2458 + },
2459 + ];
2460 + return ServerPromise;
2461 + }
2462 + const set1 = ['A', 'B', 'C'].map(getServerComponent);
2463 + const set2 = ['B', 'A', 'D'].map(getServerComponent);
2464 +
2465 + const App = ({initial}) => (initial ? set1 : set2);
2466 +
2467 + await actAsync(() => render(<App initial={true} />));
2468 + expect(store).toMatchInlineSnapshot(`
2469 + [root]
2470 + ▾ <App>
2471 + ▾ <ServerComponent> [Server]
2472 + <ClientComponent key="A">
2473 + ▾ <ServerComponent> [Server]
2474 + <ClientComponent key="B">
2475 + ▾ <ServerComponent> [Server]
2476 + <ClientComponent key="C">
2477 + `);
2478 +
2479 + await actAsync(() => render(<App initial={false} />));
2480 + expect(store).toMatchInlineSnapshot(`
2481 + [root]
2482 + ▾ <App>
2483 + ▾ <ServerComponent> [Server]
2484 + <ClientComponent key="B">
2485 + ▾ <ServerComponent> [Server]
2486 + <ClientComponent key="A">
2487 + ▾ <ServerComponent> [Server]
2488 + <ClientComponent key="D">
2489 + `);
2490 + });
2491 });
packages/react-devtools-shared/src/backend/fiber/renderer.js
+588 -85
@@ -26,6 +26,7 @@ import {
26 ElementTypeSuspense,
27 ElementTypeSuspenseList,
28 ElementTypeTracingMarker,
29 + ElementTypeVirtual,
30 StrictMode,
31 } from 'react-devtools-shared/src/frontend/types';
32 import {
@@ -134,7 +135,7 @@ import {getStackByFiberInDevAndProd} from './DevToolsFiberComponentStack';
135
136 // Kinds
137 const FIBER_INSTANCE = 0;
137 -// const VIRTUAL_INSTANCE = 1;
138 +const VIRTUAL_INSTANCE = 1;
139
140 // Flags
141 const FORCE_SUSPENSE_FALLBACK = /* */ 0b001;
@@ -197,6 +198,24 @@ type VirtualInstance = {
198 data: ReactComponentInfo,
199 };
200
201 +function createVirtualInstance(
202 + debugEntry: ReactComponentInfo,
203 +): VirtualInstance {
204 + return {
205 + kind: VIRTUAL_INSTANCE,
206 + id: getUID(),
207 + parent: null,
208 + firstChild: null,
209 + previousSibling: null,
210 + nextSibling: null,
211 + flags: 0,
212 + componentStack: null,
213 + errors: null,
214 + warnings: null,
215 + data: debugEntry,
216 + };
217 +}
218 +
219 type DevToolsInstance = FiberInstance | VirtualInstance;
220
221 type getDisplayNameForFiberType = (fiber: Fiber) => string | null;
@@ -1423,10 +1442,14 @@ export function attach(
1442 }
1443 }
1444
1426 - fiberToFiberInstanceMap.delete(fiber);
1445 + if (fiberToFiberInstanceMap.get(fiber) === fiberInstance) {
1446 + fiberToFiberInstanceMap.delete(fiber);
1447 + }
1448 const {alternate} = fiber;
1449 if (alternate !== null) {
1429 - fiberToFiberInstanceMap.delete(alternate);
1450 + if (fiberToFiberInstanceMap.get(alternate) === fiberInstance) {
1451 + fiberToFiberInstanceMap.delete(alternate);
1452 + }
1453 }
1454 }
1455
@@ -2064,15 +2087,17 @@ export function attach(
2087 throw new Error('The root should have been registered at this point');
2088 }
2089 fiberInstance = entry;
2067 - } else if (
2068 - fiberToFiberInstanceMap.has(fiber) ||
2069 - (fiber.alternate !== null && fiberToFiberInstanceMap.has(fiber.alternate))
2070 - ) {
2071 - throw new Error('Did not expect to see this fiber being mounted twice.');
2090 } else {
2091 fiberInstance = createFiberInstance(fiber);
2092 }
2093 + // If this already exists behind a different FiberInstance, we intentionally
2094 + // override it here to claim the fiber as part of this new instance.
2095 + // E.g. if it was part of a reparenting.
2096 fiberToFiberInstanceMap.set(fiber, fiberInstance);
2097 + const alternate = fiber.alternate;
2098 + if (alternate !== null && fiberToFiberInstanceMap.has(alternate)) {
2099 + fiberToFiberInstanceMap.set(alternate, fiberInstance);
2100 + }
2101 idToDevToolsInstanceMap.set(fiberInstance.id, fiberInstance);
2102
2103 const id = fiberInstance.id;
@@ -2081,20 +2106,21 @@ export function attach(
2106 debug('recordMount()', fiber, parentInstance);
2107 }
2108
2084 - const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner');
2109 const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
2110
2087 - // Adding a new field here would require a bridge protocol version bump (a backwads breaking change).
2088 - // Instead let's re-purpose a pre-existing field to carry more information.
2089 - let profilingFlags = 0;
2090 - if (isProfilingSupported) {
2091 - profilingFlags = PROFILING_FLAG_BASIC_SUPPORT;
2092 - if (typeof injectProfilingHooks === 'function') {
2093 - profilingFlags |= PROFILING_FLAG_TIMELINE_SUPPORT;
2111 + if (isRoot) {
2112 + const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner');
2113 +
2114 + // Adding a new field here would require a bridge protocol version bump (a backwads breaking change).
2115 + // Instead let's re-purpose a pre-existing field to carry more information.
2116 + let profilingFlags = 0;
2117 + if (isProfilingSupported) {
2118 + profilingFlags = PROFILING_FLAG_BASIC_SUPPORT;
2119 + if (typeof injectProfilingHooks === 'function') {
2120 + profilingFlags |= PROFILING_FLAG_TIMELINE_SUPPORT;
2121 + }
2122 }
2095 - }
2123
2097 - if (isRoot) {
2124 // Set supportsStrictMode to false for production renderer builds
2125 const isProductionBuildOfRenderer = renderer.bundleType === 0;
2126
@@ -2184,6 +2210,53 @@ export function attach(
2210 return fiberInstance;
2211 }
2212
2213 + function recordVirtualMount(
2214 + instance: VirtualInstance,
2215 + parentInstance: DevToolsInstance | null,
2216 + ): void {
2217 + const id = instance.id;
2218 +
2219 + idToDevToolsInstanceMap.set(id, instance);
2220 +
2221 + const isProfilingSupported = false; // TODO: Support Tree Base Duration Based on Children.
2222 +
2223 + const key = null; // TODO: Track keys on ReactComponentInfo;
2224 + const env = instance.data.env;
2225 + let displayName = instance.data.name || '';
2226 + if (typeof env === 'string') {
2227 + // We model environment as an HoC name for now.
2228 + displayName = env + '(' + displayName + ')';
2229 + }
2230 + const elementType = ElementTypeVirtual;
2231 + // TODO: Support Virtual Owners. To do this we need to find a matching
2232 + // virtual instance which is not a super cheap parent traversal and so
2233 + // we should ideally only do that lazily. We should maybe change the
2234 + // frontend to get it lazily.
2235 + const ownerID: number = 0;
2236 + const parentID = parentInstance ? parentInstance.id : 0;
2237 +
2238 + const displayNameStringID = getStringID(displayName);
2239 +
2240 + // This check is a guard to handle a React element that has been modified
2241 + // in such a way as to bypass the default stringification of the "key" property.
2242 + const keyString = key === null ? null : String(key);
2243 + const keyStringID = getStringID(keyString);
2244 +
2245 + pushOperation(TREE_OPERATION_ADD);
2246 + pushOperation(id);
2247 + pushOperation(elementType);
2248 + pushOperation(parentID);
2249 + pushOperation(ownerID);
2250 + pushOperation(displayNameStringID);
2251 + pushOperation(keyStringID);
2252 +
2253 + if (isProfilingSupported) {
2254 + idToRootMap.set(id, currentRootID);
2255 + // TODO: Include tree base duration of children somehow.
2256 + // recordProfilingDurations(...);
2257 + }
2258 + }
2259 +
2260 function recordUnmount(fiberInstance: FiberInstance): void {
2261 const fiber = fiberInstance.data;
2262 if (__DEBUG__) {
@@ -2261,6 +2334,15 @@ export function attach(
2334
2335 function removeChild(instance: DevToolsInstance): void {
2336 if (instance.parent === null) {
2337 + if (remainingReconcilingChildren === instance) {
2338 + throw new Error(
2339 + 'Remaining children should not have items with no parent',
2340 + );
2341 + } else if (instance.nextSibling !== null) {
2342 + throw new Error('A deleted instance should not have next siblings');
2343 + } else if (instance.previousSibling !== null) {
2344 + throw new Error('A deleted instance should not have previous siblings');
2345 + }
2346 // Already deleted.
2347 return;
2348 }
@@ -2302,17 +2384,142 @@ export function attach(
2384 }
2385 }
2386
2305 - function mountChildrenRecursively(
2387 + function mountVirtualInstanceRecursively(
2388 + virtualInstance: VirtualInstance,
2389 firstChild: Fiber,
2390 + lastChild: null | Fiber, // non-inclusive
2391 traceNearestHostComponentUpdate: boolean,
2392 + virtualLevel: number, // the nth level of virtual instances
2393 + ): void {
2394 + const stashedParent = reconcilingParent;
2395 + const stashedPrevious = previouslyReconciledSibling;
2396 + const stashedRemaining = remainingReconcilingChildren;
2397 + // Push a new DevTools instance parent while reconciling this subtree.
2398 + reconcilingParent = virtualInstance;
2399 + previouslyReconciledSibling = null;
2400 + remainingReconcilingChildren = null;
2401 + try {
2402 + mountVirtualChildrenRecursively(
2403 + firstChild,
2404 + lastChild,
2405 + traceNearestHostComponentUpdate,
2406 + virtualLevel + 1,
2407 + );
2408 + } finally {
2409 + reconcilingParent = stashedParent;
2410 + previouslyReconciledSibling = stashedPrevious;
2411 + remainingReconcilingChildren = stashedRemaining;
2412 + }
2413 + }
2414 +
2415 + function recordVirtualUnmount(instance: VirtualInstance) {
2416 + if (trackedPathMatchFiber !== null) {
2417 + // We're in the process of trying to restore previous selection.
2418 + // TODO: Handle virtual instances on the tracked path.
2419 + }
2420 +
2421 + const id = instance.id;
2422 + pendingRealUnmountedIDs.push(id);
2423 +
2424 + const isProfilingSupported = false; // TODO: Profiling support.
2425 + if (isProfilingSupported) {
2426 + idToRootMap.delete(id);
2427 + idToTreeBaseDurationMap.delete(id);
2428 + }
2429 + }
2430 +
2431 + function mountVirtualChildrenRecursively(
2432 + firstChild: Fiber,
2433 + lastChild: null | Fiber, // non-inclusive
2434 + traceNearestHostComponentUpdate: boolean,
2435 + virtualLevel: number, // the nth level of virtual instances
2436 ): void {
2437 // Iterate over siblings rather than recursing.
2438 // This reduces the chance of stack overflow for wide trees (e.g. lists with many items).
2439 let fiber: Fiber | null = firstChild;
2312 - while (fiber !== null) {
2313 - mountFiberRecursively(fiber, traceNearestHostComponentUpdate);
2440 + let previousVirtualInstance: null | VirtualInstance = null;
2441 + let previousVirtualInstanceFirstFiber: Fiber = firstChild;
2442 + while (fiber !== null && fiber !== lastChild) {
2443 + let level = 0;
2444 + if (fiber._debugInfo) {
2445 + for (let i = 0; i < fiber._debugInfo.length; i++) {
2446 + const debugEntry = fiber._debugInfo[i];
2447 + if (typeof debugEntry.name !== 'string') {
2448 + // Not a Component. Some other Debug Info.
2449 + continue;
2450 + }
2451 + const componentInfo: ReactComponentInfo = (debugEntry: any);
2452 + if (level === virtualLevel) {
2453 + if (
2454 + previousVirtualInstance === null ||
2455 + // Consecutive children with the same debug entry as a parent gets
2456 + // treated as if they share the same virtual instance.
2457 + previousVirtualInstance.data !== debugEntry
2458 + ) {
2459 + if (previousVirtualInstance !== null) {
2460 + // Mount any previous children that should go into the previous parent.
2461 + mountVirtualInstanceRecursively(
2462 + previousVirtualInstance,
2463 + previousVirtualInstanceFirstFiber,
2464 + fiber,
2465 + traceNearestHostComponentUpdate,
2466 + virtualLevel,
2467 + );
2468 + }
2469 + previousVirtualInstance = createVirtualInstance(componentInfo);
2470 + recordVirtualMount(previousVirtualInstance, reconcilingParent);
2471 + insertChild(previousVirtualInstance);
2472 + previousVirtualInstanceFirstFiber = fiber;
2473 + }
2474 + level++;
2475 + break;
2476 + } else {
2477 + level++;
2478 + }
2479 + }
2480 + }
2481 + if (level === virtualLevel) {
2482 + if (previousVirtualInstance !== null) {
2483 + // If we were working on a virtual instance and this is not a virtual
2484 + // instance, then we end the sequence and mount any previous children
2485 + // that should go into the previous virtual instance.
2486 + mountVirtualInstanceRecursively(
2487 + previousVirtualInstance,
2488 + previousVirtualInstanceFirstFiber,
2489 + fiber,
2490 + traceNearestHostComponentUpdate,
2491 + virtualLevel,
2492 + );
2493 + previousVirtualInstance = null;
2494 + }
2495 + // We've reached the end of the virtual levels, but not beyond,
2496 + // and now continue with the regular fiber.
2497 + mountFiberRecursively(fiber, traceNearestHostComponentUpdate);
2498 + }
2499 fiber = fiber.sibling;
2500 }
2501 + if (previousVirtualInstance !== null) {
2502 + // Mount any previous children that should go into the previous parent.
2503 + mountVirtualInstanceRecursively(
2504 + previousVirtualInstance,
2505 + previousVirtualInstanceFirstFiber,
2506 + null,
2507 + traceNearestHostComponentUpdate,
2508 + virtualLevel,
2509 + );
2510 + }
2511 + }
2512 +
2513 + function mountChildrenRecursively(
2514 + firstChild: Fiber,
2515 + traceNearestHostComponentUpdate: boolean,
2516 + ): void {
2517 + mountVirtualChildrenRecursively(
2518 + firstChild,
2519 + null,
2520 + traceNearestHostComponentUpdate,
2521 + 0, // first level
2522 + );
2523 }
2524
2525 function mountFiberRecursively(
@@ -2435,6 +2642,7 @@ export function attach(
2642 previouslyReconciledSibling = null;
2643 // Move all the children of this instance to the remaining set.
2644 remainingReconcilingChildren = instance.firstChild;
2645 + instance.firstChild = null;
2646 try {
2647 // Unmount the remaining set.
2648 unmountRemainingChildren();
@@ -2445,6 +2653,8 @@ export function attach(
2653 }
2654 if (instance.kind === FIBER_INSTANCE) {
2655 recordUnmount(instance);
2656 + } else {
2657 + recordVirtualUnmount(instance);
2658 }
2659 removeChild(instance);
2660 }
@@ -2553,57 +2763,196 @@ export function attach(
2763 }
2764 }
2765
2556 - // Returns whether closest unfiltered fiber parent needs to reset its child list.
2557 - function updateChildrenRecursively(
2558 - nextFirstChild: null | Fiber,
2766 + function updateVirtualInstanceRecursively(
2767 + virtualInstance: VirtualInstance,
2768 + nextFirstChild: Fiber,
2769 + nextLastChild: null | Fiber, // non-inclusive
2770 prevFirstChild: null | Fiber,
2771 traceNearestHostComponentUpdate: boolean,
2772 + virtualLevel: number, // the nth level of virtual instances
2773 + ): void {
2774 + const stashedParent = reconcilingParent;
2775 + const stashedPrevious = previouslyReconciledSibling;
2776 + const stashedRemaining = remainingReconcilingChildren;
2777 + // Push a new DevTools instance parent while reconciling this subtree.
2778 + reconcilingParent = virtualInstance;
2779 + previouslyReconciledSibling = null;
2780 + // Move all the children of this instance to the remaining set.
2781 + // We'll move them back one by one, and anything that remains is deleted.
2782 + remainingReconcilingChildren = virtualInstance.firstChild;
2783 + virtualInstance.firstChild = null;
2784 + try {
2785 + if (
2786 + updateVirtualChildrenRecursively(
2787 + nextFirstChild,
2788 + nextLastChild,
2789 + prevFirstChild,
2790 + traceNearestHostComponentUpdate,
2791 + virtualLevel + 1,
2792 + )
2793 + ) {
2794 + recordResetChildren(virtualInstance);
2795 + }
2796 + } finally {
2797 + unmountRemainingChildren();
2798 + reconcilingParent = stashedParent;
2799 + previouslyReconciledSibling = stashedPrevious;
2800 + remainingReconcilingChildren = stashedRemaining;
2801 + }
2802 + }
2803 +
2804 + function updateVirtualChildrenRecursively(
2805 + nextFirstChild: Fiber,
2806 + nextLastChild: null | Fiber, // non-inclusive
2807 + prevFirstChild: null | Fiber,
2808 + traceNearestHostComponentUpdate: boolean,
2809 + virtualLevel: number, // the nth level of virtual instances
2810 ): boolean {
2811 let shouldResetChildren = false;
2812 // If the first child is different, we need to traverse them.
2813 // Each next child will be either a new child (mount) or an alternate (update).
2565 - let nextChild = nextFirstChild;
2814 + let nextChild: null | Fiber = nextFirstChild;
2815 let prevChildAtSameIndex = prevFirstChild;
2567 - while (nextChild) {
2568 - // We already know children will be referentially different because
2569 - // they are either new mounts or alternates of previous children.
2570 - // Schedule updates and mounts depending on whether alternates exist.
2571 - // We don't track deletions here because they are reported separately.
2572 - if (prevChildAtSameIndex === nextChild) {
2573 - // This set is unchanged. We're just going through it to place all the
2574 - // children again.
2575 - if (
2576 - updateFiberRecursively(
2577 - nextChild,
2578 - nextChild,
2579 - traceNearestHostComponentUpdate,
2580 - )
2581 - ) {
2582 - throw new Error('Updating the same fiber should not cause reorder');
2816 + let previousVirtualInstance: null | VirtualInstance = null;
2817 + let previousVirtualInstanceWasMount: boolean = false;
2818 + let previousVirtualInstanceNextFirstFiber: Fiber = nextFirstChild;
2819 + let previousVirtualInstancePrevFirstFiber: null | Fiber = prevFirstChild;
2820 + while (nextChild !== null && nextChild !== nextLastChild) {
2821 + let level = 0;
2822 + if (nextChild._debugInfo) {
2823 + for (let i = 0; i < nextChild._debugInfo.length; i++) {
2824 + const debugEntry = nextChild._debugInfo[i];
2825 + if (typeof debugEntry.name !== 'string') {
2826 + // Not a Component. Some other Debug Info.
2827 + continue;
2828 + }
2829 + const componentInfo: ReactComponentInfo = (debugEntry: any);
2830 + if (level === virtualLevel) {
2831 + if (
2832 + previousVirtualInstance === null ||
2833 + // Consecutive children with the same debug entry as a parent gets
2834 + // treated as if they share the same virtual instance.
2835 + previousVirtualInstance.data !== componentInfo
2836 + ) {
2837 + if (previousVirtualInstance !== null) {
2838 + // Mount any previous children that should go into the previous parent.
2839 + if (previousVirtualInstanceWasMount) {
2840 + mountVirtualInstanceRecursively(
2841 + previousVirtualInstance,
2842 + previousVirtualInstanceNextFirstFiber,
2843 + nextChild,
2844 + traceNearestHostComponentUpdate,
2845 + virtualLevel,
2846 + );
2847 + } else {
2848 + updateVirtualInstanceRecursively(
2849 + previousVirtualInstance,
2850 + previousVirtualInstanceNextFirstFiber,
2851 + nextChild,
2852 + previousVirtualInstancePrevFirstFiber,
2853 + traceNearestHostComponentUpdate,
2854 + virtualLevel,
2855 + );
2856 + }
2857 + }
2858 + const firstRemainingChild = remainingReconcilingChildren;
2859 + if (
2860 + firstRemainingChild !== null &&
2861 + firstRemainingChild.kind === VIRTUAL_INSTANCE &&
2862 + firstRemainingChild.data.name === componentInfo.name &&
2863 + firstRemainingChild.data.env === componentInfo.env
2864 + ) {
2865 + // If the previous children had a virtual instance in the same slot
2866 + // with the same name, then we claim it and reuse it for this update.
2867 + // Update it with the latest entry.
2868 + firstRemainingChild.data = componentInfo;
2869 + moveChild(firstRemainingChild);
2870 + previousVirtualInstance = firstRemainingChild;
2871 + previousVirtualInstanceWasMount = false;
2872 + } else {
2873 + // Otherwise we create a new instance.
2874 + const newVirtualInstance = createVirtualInstance(componentInfo);
2875 + recordVirtualMount(newVirtualInstance, reconcilingParent);
2876 + insertChild(newVirtualInstance);
2877 + previousVirtualInstance = newVirtualInstance;
2878 + previousVirtualInstanceWasMount = true;
2879 + shouldResetChildren = true;
2880 + }
2881 + // Existing children might be reparented into this new virtual instance.
2882 + // TODO: This will cause the front end to error which needs to be fixed.
2883 + previousVirtualInstanceNextFirstFiber = nextChild;
2884 + previousVirtualInstancePrevFirstFiber = prevChildAtSameIndex;
2885 + }
2886 + level++;
2887 + break;
2888 + } else {
2889 + level++;
2890 + }
2891 }
2584 - } else if (nextChild.alternate) {
2585 - const prevChild = nextChild.alternate;
2586 - if (
2587 - updateFiberRecursively(
2588 - nextChild,
2589 - prevChild,
2590 - traceNearestHostComponentUpdate,
2591 - )
2592 - ) {
2593 - // If a nested tree child order changed but it can't handle its own
2594 - // child order invalidation (e.g. because it's filtered out like host nodes),
2595 - // propagate the need to reset child order upwards to this Fiber.
2596 - shouldResetChildren = true;
2892 + }
2893 + if (level === virtualLevel) {
2894 + if (previousVirtualInstance !== null) {
2895 + // If we were working on a virtual instance and this is not a virtual
2896 + // instance, then we end the sequence and update any previous children
2897 + // that should go into the previous virtual instance.
2898 + if (previousVirtualInstanceWasMount) {
2899 + mountVirtualInstanceRecursively(
2900 + previousVirtualInstance,
2901 + previousVirtualInstanceNextFirstFiber,
2902 + nextChild,
2903 + traceNearestHostComponentUpdate,
2904 + virtualLevel,
2905 + );
2906 + } else {
2907 + updateVirtualInstanceRecursively(
2908 + previousVirtualInstance,
2909 + previousVirtualInstanceNextFirstFiber,
2910 + nextChild,
2911 + previousVirtualInstancePrevFirstFiber,
2912 + traceNearestHostComponentUpdate,
2913 + virtualLevel,
2914 + );
2915 + }
2916 + previousVirtualInstance = null;
2917 }
2598 - // However we also keep track if the order of the children matches
2599 - // the previous order. They are always different referentially, but
2600 - // if the instances line up conceptually we'll want to know that.
2601 - if (prevChild !== prevChildAtSameIndex) {
2918 + // We've reached the end of the virtual levels, but not beyond,
2919 + // and now continue with the regular fiber.
2920 + if (prevChildAtSameIndex === nextChild) {
2921 + // This set is unchanged. We're just going through it to place all the
2922 + // children again.
2923 + if (
2924 + updateFiberRecursively(
2925 + nextChild,
2926 + nextChild,
2927 + traceNearestHostComponentUpdate,
2928 + )
2929 + ) {
2930 + throw new Error('Updating the same fiber should not cause reorder');
2931 + }
2932 + } else if (nextChild.alternate) {
2933 + const prevChild = nextChild.alternate;
2934 + if (
2935 + updateFiberRecursively(
2936 + nextChild,
2937 + prevChild,
2938 + traceNearestHostComponentUpdate,
2939 + )
2940 + ) {
2941 + // If a nested tree child order changed but it can't handle its own
2942 + // child order invalidation (e.g. because it's filtered out like host nodes),
2943 + // propagate the need to reset child order upwards to this Fiber.
2944 + shouldResetChildren = true;
2945 + }
2946 + // However we also keep track if the order of the children matches
2947 + // the previous order. They are always different referentially, but
2948 + // if the instances line up conceptually we'll want to know that.
2949 + if (prevChild !== prevChildAtSameIndex) {
2950 + shouldResetChildren = true;
2951 + }
2952 + } else {
2953 + mountFiberRecursively(nextChild, traceNearestHostComponentUpdate);
2954 shouldResetChildren = true;
2955 }
2604 - } else {
2605 - mountFiberRecursively(nextChild, traceNearestHostComponentUpdate);
2606 - shouldResetChildren = true;
2956 }
2957 // Try the next child.
2958 nextChild = nextChild.sibling;
@@ -2613,6 +2962,26 @@ export function attach(
2962 prevChildAtSameIndex = prevChildAtSameIndex.sibling;
2963 }
2964 }
2965 + if (previousVirtualInstance !== null) {
2966 + if (previousVirtualInstanceWasMount) {
2967 + mountVirtualInstanceRecursively(
2968 + previousVirtualInstance,
2969 + previousVirtualInstanceNextFirstFiber,
2970 + null,
2971 + traceNearestHostComponentUpdate,
2972 + virtualLevel,
2973 + );
2974 + } else {
2975 + updateVirtualInstanceRecursively(
2976 + previousVirtualInstance,
2977 + previousVirtualInstanceNextFirstFiber,
2978 + null,
2979 + previousVirtualInstancePrevFirstFiber,
2980 + traceNearestHostComponentUpdate,
2981 + virtualLevel,
2982 + );
2983 + }
2984 + }
2985 // If we have no more children, but used to, they don't line up.
2986 if (prevChildAtSameIndex !== null) {
2987 shouldResetChildren = true;
@@ -2620,6 +2989,24 @@ export function attach(
2989 return shouldResetChildren;
2990 }
2991
2992 + // Returns whether closest unfiltered fiber parent needs to reset its child list.
2993 + function updateChildrenRecursively(
2994 + nextFirstChild: null | Fiber,
2995 + prevFirstChild: null | Fiber,
2996 + traceNearestHostComponentUpdate: boolean,
2997 + ): boolean {
2998 + if (nextFirstChild === null) {
2999 + return prevFirstChild !== null;
3000 + }
3001 + return updateVirtualChildrenRecursively(
3002 + nextFirstChild,
3003 + null,
3004 + prevFirstChild,
3005 + traceNearestHostComponentUpdate,
3006 + 0,
3007 + );
3008 + }
3009 +
3010 // Returns whether closest unfiltered fiber parent needs to reset its child list.
3011 function updateFiberRecursively(
3012 nextFiber: Fiber,
@@ -2659,18 +3046,29 @@ export function attach(
3046 const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
3047 if (shouldIncludeInTree) {
3048 const entry = fiberToFiberInstanceMap.get(prevFiber);
2662 - if (entry === undefined) {
2663 - throw new Error(
2664 - 'The previous version of the fiber should have already been registered.',
2665 - );
2666 - }
2667 - fiberInstance = entry;
2668 - // Register the new alternate in case it's not already in.
2669 - fiberToFiberInstanceMap.set(nextFiber, fiberInstance);
3049 + if (entry !== undefined && entry.parent === reconcilingParent) {
3050 + // Common case. Match in the same parent.
3051 + fiberInstance = entry;
3052 + // Register the new alternate in case it's not already in.
3053 + fiberToFiberInstanceMap.set(nextFiber, fiberInstance);
3054 +
3055 + // Update the Fiber so we that we always keep the current Fiber on the data.
3056 + fiberInstance.data = nextFiber;
3057 + moveChild(fiberInstance);
3058 + } else {
3059 + // It's possible for a FiberInstance to be reparented when virtual parents
3060 + // get their sequence split or change structure with the same render result.
3061 + // In this case we unmount the and remount the FiberInstances.
3062 + // This might cause us to lose the selection but it's an edge case.
3063
2671 - // Update the Fiber so we that we always keep the current Fiber on the data.
2672 - fiberInstance.data = nextFiber;
2673 - moveChild(fiberInstance);
3064 + // We let the previous instance remain in the "remaining queue" it is
3065 + // in to be deleted at the end since it'll have no match.
3066 +
3067 + mountFiberRecursively(nextFiber, traceNearestHostComponentUpdate);
3068 +
3069 + // Need to mark the parent set to remount the new instance.
3070 + return true;
3071 + }
3072
3073 if (
3074 mostRecentlyInspectedElement !== null &&
@@ -3615,12 +4013,16 @@ export function attach(
4013 console.warn(`Could not find DevToolsInstance with id "${id}"`);
4014 return null;
4015 }
3618 - if (devtoolsInstance.kind !== FIBER_INSTANCE) {
3619 - // TODO: Handle VirtualInstance.
3620 - return null;
4016 + if (devtoolsInstance.kind === VIRTUAL_INSTANCE) {
4017 + return inspectVirtualInstanceRaw(devtoolsInstance);
4018 }
3622 - const fiber =
3623 - findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
4019 + return inspectFiberInstanceRaw(devtoolsInstance);
4020 + }
4021 +
4022 + function inspectFiberInstanceRaw(
4023 + fiberInstance: FiberInstance,
4024 + ): InspectedElement | null {
4025 + const fiber = findCurrentFiberUsingSlowPathByFiberInstance(fiberInstance);
4026 if (fiber == null) {
4027 return null;
4028 }
@@ -3823,8 +4225,10 @@ export function attach(
4225 const DidCapture = 0b000000000000000000010000000;
4226 isErrored =
4227 (fiber.flags & DidCapture) !== 0 ||
3826 - (devtoolsInstance.flags & FORCE_ERROR) !== 0;
3827 - targetErrorBoundaryID = isErrored ? id : getNearestErrorBoundaryID(fiber);
4228 + (fiberInstance.flags & FORCE_ERROR) !== 0;
4229 + targetErrorBoundaryID = isErrored
4230 + ? fiberInstance.id
4231 + : getNearestErrorBoundaryID(fiber);
4232 } else {
4233 targetErrorBoundaryID = getNearestErrorBoundaryID(fiber);
4234 }
@@ -3845,7 +4249,7 @@ export function attach(
4249 }
4250
4251 return {
3848 - id,
4252 + id: fiberInstance.id,
4253
4254 // Does the current renderer support editable hooks and function props?
4255 canEditHooks: typeof overrideHookState === 'function',
@@ -3872,7 +4276,7 @@ export function attach(
4276 (!isTimedOutSuspense ||
4277 // If it's showing fallback because we previously forced it to,
4278 // allow toggling it back to remove the fallback override.
3875 - (devtoolsInstance.flags & FORCE_SUSPENSE_FALLBACK) !== 0),
4279 + (fiberInstance.flags & FORCE_SUSPENSE_FALLBACK) !== 0),
4280
4281 // Can view component source location.
4282 canViewSource,
@@ -3893,13 +4297,112 @@ export function attach(
4297 props: memoizedProps,
4298 state: showState ? memoizedState : null,
4299 errors:
3896 - devtoolsInstance.errors === null
4300 + fiberInstance.errors === null
4301 + ? []
4302 + : Array.from(fiberInstance.errors.entries()),
4303 + warnings:
4304 + fiberInstance.warnings === null
4305 + ? []
4306 + : Array.from(fiberInstance.warnings.entries()),
4307 +
4308 + // List of owners
4309 + owners,
4310 +
4311 + rootType,
4312 + rendererPackageName: renderer.rendererPackageName,
4313 + rendererVersion: renderer.version,
4314 +
4315 + plugins,
4316 + };
4317 + }
4318 +
4319 + function inspectVirtualInstanceRaw(
4320 + virtualInstance: VirtualInstance,
4321 + ): InspectedElement | null {
4322 + const canViewSource = false;
4323 +
4324 + const key = null; // TODO: Track keys on ReactComponentInfo;
4325 + const props = null; // TODO: Track props on ReactComponentInfo;
4326 +
4327 + const env = virtualInstance.data.env;
4328 + let displayName = virtualInstance.data.name || '';
4329 + if (typeof env === 'string') {
4330 + // We model environment as an HoC name for now.
4331 + displayName = env + '(' + displayName + ')';
4332 + }
4333 +
4334 + // TODO: Support Virtual Owners.
4335 + const owners: null | Array<SerializedElement> = null;
4336 +
4337 + let rootType = null;
4338 + let targetErrorBoundaryID = null;
4339 + let parent = virtualInstance.parent;
4340 + while (parent !== null) {
4341 + if (parent.kind === FIBER_INSTANCE) {
4342 + targetErrorBoundaryID = getNearestErrorBoundaryID(parent.data);
4343 + let current = parent.data;
4344 + while (current.return !== null) {
4345 + current = current.return;
4346 + }
4347 + const fiberRoot = current.stateNode;
4348 + if (fiberRoot != null && fiberRoot._debugRootType !== null) {
4349 + rootType = fiberRoot._debugRootType;
4350 + }
4351 + break;
4352 + }
4353 + parent = parent.parent;
4354 + }
4355 +
4356 + const plugins: Plugins = {
4357 + stylex: null,
4358 + };
4359 +
4360 + // TODO: Support getting the source location from the owner stack.
4361 + const source = null;
4362 +
4363 + return {
4364 + id: virtualInstance.id,
4365 +
4366 + canEditHooks: false,
4367 + canEditFunctionProps: false,
4368 +
4369 + canEditHooksAndDeletePaths: false,
4370 + canEditHooksAndRenamePaths: false,
4371 + canEditFunctionPropsDeletePaths: false,
4372 + canEditFunctionPropsRenamePaths: false,
4373 +
4374 + canToggleError: supportsTogglingError && targetErrorBoundaryID != null,
4375 + isErrored: false,
4376 + targetErrorBoundaryID,
4377 +
4378 + canToggleSuspense: supportsTogglingSuspense,
4379 +
4380 + // Can view component source location.
4381 + canViewSource,
4382 + source,
4383 +
4384 + // Does the component have legacy context attached to it.
4385 + hasLegacyContext: false,
4386 +
4387 + key: key != null ? key : null,
4388 +
4389 + displayName: displayName,
4390 + type: ElementTypeVirtual,
4391 +
4392 + // Inspectable properties.
4393 + // TODO Review sanitization approach for the below inspectable values.
4394 + context: null,
4395 + hooks: null,
4396 + props: props,
4397 + state: null,
4398 + errors:
4399 + virtualInstance.errors === null
4400 ? []
3898 - : Array.from(devtoolsInstance.errors.entries()),
4401 + : Array.from(virtualInstance.errors.entries()),
4402 warnings:
3900 - devtoolsInstance.warnings === null
4403 + virtualInstance.warnings === null
4404 ? []
3902 - : Array.from(devtoolsInstance.warnings.entries()),
4405 + : Array.from(virtualInstance.warnings.entries()),
4406
4407 // List of owners
4408 owners,
packages/react-devtools-shared/src/frontend/types.js
+15 -1
@@ -48,11 +48,25 @@ export const ElementTypeRoot = 11;
48 export const ElementTypeSuspense = 12;
49 export const ElementTypeSuspenseList = 13;
50 export const ElementTypeTracingMarker = 14;
51 +export const ElementTypeVirtual = 15;
52
53 // Different types of elements displayed in the Elements tree.
54 // These types may be used to visually distinguish types,
55 // or to enable/disable certain functionality.
55 -export type ElementType = 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14;
56 +export type ElementType =
57 + | 1
58 + | 2
59 + | 5
60 + | 6
61 + | 7
62 + | 8
63 + | 9
64 + | 10
65 + | 11
66 + | 12
67 + | 13
68 + | 14
69 + | 15;
70
71 // WARNING
72 // The values below are referenced by ComponentFilters (which are saved via localStorage).
packages/react-devtools-shared/src/utils.js
+2
@@ -66,6 +66,7 @@ import {
66 ElementTypeForwardRef,
67 ElementTypeFunction,
68 ElementTypeMemo,
69 + ElementTypeVirtual,
70 } from 'react-devtools-shared/src/frontend/types';
71 import {localStorageGetItem, localStorageSetItem} from './storage';
72 import {meta} from './hydration';
@@ -484,6 +485,7 @@ export function parseElementDisplayNameFromBackend(
485 case ElementTypeForwardRef:
486 case ElementTypeFunction:
487 case ElementTypeMemo:
488 + case ElementTypeVirtual:
489 if (displayName.indexOf('(') >= 0) {
490 const matches = displayName.match(/[^()]+/g);
491 if (matches != null) {