Repository: facebook/react
Pull request: [DevTools] Add DevToolsInstance to Store Stateful Information

Stacked on #30494 and #30491.

This is setting us up to be able to track Server Components. This is now split into a FiberInstance (Client Component) and a VirtualInstance (Server Component). We're not actually creating any VirtualInstances yet though this is just the data structures.

Server Components and potentially other compiled away or runtime optimized away (e.g. calling through a function without creating an intermediate Fiber) don't have a stateful instance. They just represent the latest data. It's kind of like a React Element.

However, in the DevTools UI we need them to be stateful partly just so that you can select and refer to them separately. E.g. the same Server Component output rendered into different slots on the client should still have two different representations in the DevTools. Also if the same child Fibers update in place because the Server Component refreshed we shouldn't lose the selection if you've selected a Server Component.

I'll implement this by creating Virtual Instances that only exist for the purpose of the DevTools UI and so it'll be implemented in the DevTools.

We could just make a Map from `id` to `Fiber | ReactComponentInfo` but that requires a branching without a consistent hidden class. We also need some more states on there. We also have some other Maps that tracks extra states like that of component stacks, errors and warnings. Constantly resizing and adding/removing from a Map isn't exactly fast. It's faster to have a single Map with an object in it than one Map per object. However, having extra fields that are usually just `null` can instead mean more memory gets used. Since only a very small fraction of instances will have errors/warnings or having initialized its component stack, it made sense to store that in a separate Map that is usually just empty.

However, with the addition of particularly the `parent` field and the ability to do a fast hidden-class safe branching on the `kind` field I think it's probably worth actually allocating an extra first class object per Instance to store DevTools state into. That's why I converted from just storing `Fiber` -> `id` to storing `Fiber` -> `DevToolsInstance` which then keeps the warnings/errors/componentStack as extra fields that are usually `null`. That is a lot of objects though since it's one per Fiber pair basically.

Changed files:
filename: packages/react-devtools-shared/src/backend/fiber/renderer.js
patch: @@ -7,6 +7,8 @@
  * @flow
  */
 
+import type {ReactComponentInfo} from 'shared/ReactTypes';
+
 import {
   ComponentFilterDisplayName,
   ComponentFilterElementType,
@@ -131,6 +133,64 @@ import type {
 import type {Source} from 'react-devtools-shared/src/shared/types';
 import {getStackByFiberInDevAndProd} from './DevToolsFiberComponentStack';
 
+// Kinds
+const FIBER_INSTANCE = 0;
+// const VIRTUAL_INSTANCE = 1;
+
+// Flags
+const FORCE_SUSPENSE_FALLBACK = /*    */ 0b001;
+const FORCE_ERROR = /*                */ 0b010;
+const FORCE_ERROR_RESET = /*          */ 0b100;
+
+// This type represents a stateful instance of a Client Component i.e. a Fiber pair.
+// These instances also let us track stateful DevTools meta data like id and warnings.
+type FiberInstance = {
+  kind: 0,
+  id: number,
+  parent: null | DevToolsInstance, // virtual parent
+  flags: number, // Force Error/Suspense
+  componentStack: null | string,
+  errors: null | Map<string, number>, // error messages and count
+  warnings: null | Map<string, number>, // warning messages and count
+  data: Fiber, // one of a Fiber pair
+};
+
+function createFiberInstance(fiber: Fiber): FiberInstance {
+  return {
+    kind: 0,
+    id: getUID(),
+    parent: null,
+    flags: 0,
+    componentStack: null,
+    errors: null,
+    warnings: null,
+    data: fiber,
+  };
+}
+
+// This type represents a stateful instance of a Server Component or a Component
+// that gets optimized away - e.g. call-through without creating a Fiber.
+// It's basically a virtual Fiber. This is not a semantic concept in React.
+// It only exists as a virtual concept to let the same Element in the DevTools
+// persist. To be selectable separately from all ReactComponentInfo and overtime.
+type VirtualInstance = {
+  kind: 1,
+  id: number,
+  parent: null | DevToolsInstance, // virtual parent
+  flags: number,
+  componentStack: null | string,
+  // Errors and Warnings happen per ReactComponentInfo which can appear in
+  // multiple places but we track them per stateful VirtualInstance so
+  // that old errors/warnings don't disappear when the instance is refreshed.
+  errors: null | Map<string, number>, // error messages and count
+  warnings: null | Map<string, number>, // warning messages and count
+  // The latest info for this instance. This can be updated over time and the
+  // same info can appear in more than once ServerComponentInstance.
+  data: ReactComponentInfo,
+};
+
+type DevToolsInstance = FiberInstance | VirtualInstance;
+
 type getDisplayNameForFiberType = (fiber: Fiber) => string | null;
 type getTypeSymbolType = (type: any) => symbol | number;
 
@@ -629,14 +689,12 @@ export function getInternalReactConstants(version: string): {
 // We track both Fibers to support Fast Refresh,
 // which may forcefully replace one of the pair as part of hot reloading.
 // In that case it's still important to be able to locate the previous ID during subsequent renders.
-const fiberToIDMap: Map<Fiber, number> = new Map();
+const fiberToFiberInstanceMap: Map<Fiber, FiberInstance> = new Map();
 
 // Map of id to one (arbitrary) Fiber in a pair.
 // This Map is used to e.g. get the display name for a Fiber or schedule an update,
 // operations that should be the same whether the current and work-in-progress Fiber is used.
-const idToArbitraryFiberMap: Map<number, Fiber> = new Map();
-
-const fiberToComponentStackMap: WeakMap<Fiber, string> = new WeakMap();
+const idToDevToolsInstanceMap: Map<number, DevToolsInstance> = new Map();
 
 export function attach(
   hook: DevToolsHook,
@@ -750,81 +808,80 @@ export function attach(
   }
 
   // Tracks Fibers with recently changed number of error/warning messages.
-  // These collections store the Fiber rather than the ID,
-  // in order to avoid generating an ID for Fibers that never get mounted
+  // These collections store the Fiber rather than the DevToolsInstance,
+  // in order to avoid generating an DevToolsInstance for Fibers that never get mounted
   // (due to e.g. Suspense or error boundaries).
   // onErrorOrWarning() adds Fibers and recordPendingErrorsAndWarnings() later clears them.
   const fibersWithChangedErrorOrWarningCounts: Set<Fiber> = new Set();
   const pendingFiberToErrorsMap: Map<Fiber, Map<string, number>> = new Map();
   const pendingFiberToWarningsMap: Map<Fiber, Map<string, number>> = new Map();
 
-  // Mapping of fiber IDs to error/warning messages and counts.
-  const fiberIDToErrorsMap: Map<number, Map<string, number>> = new Map();
-  const fiberIDToWarningsMap: Map<number, Map<string, number>> = new Map();
-
   function clearErrorsAndWarnings() {
     // eslint-disable-next-line no-for-of-loops/no-for-of-loops
-    for (const id of fiberIDToErrorsMap.keys()) {
-      const fiber = idToArbitraryFiberMap.get(id);
-      if (fiber != null) {
-        fibersWithChangedErrorOrWarningCounts.add(fiber);
-        updateMostRecentlyInspectedElementIfNecessary(id);
-      }
-    }
-
-    // eslint-disable-next-line no-for-of-loops/no-for-of-loops
-    for (const id of fiberIDToWarningsMap.keys()) {
-      const fiber = idToArbitraryFiberMap.get(id);
-      if (fiber != null) {
-        fibersWithChangedErrorOrWarningCounts.add(fiber);
-        updateMostRecentlyInspectedElementIfNecessary(id);
+    for (const devtoolsInstance of idToDevToolsInstanceMap.values()) {
+      devtoolsInstance.errors = null;
+      devtoolsInstance.warnings = null;
+      if (devtoolsInstance.kind === FIBER_INSTANCE) {
+        fibersWithChangedErrorOrWarningCounts.add(devtoolsInstance.data);
+      } else {
+        // TODO: Handle VirtualInstance.
       }
+      updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);
     }
-
-    fiberIDToErrorsMap.clear();
-    fiberIDToWarningsMap.clear();
-
     flushPendingEvents();
   }
 
   function clearMessageCountHelper(
-    fiberID: number,
+    instanceID: number,
     pendingFiberToMessageCountMap: Map<Fiber, Map<string, number>>,
-    fiberIDToMessageCountMap: Map<number, Map<string, number>>,
+    forError: boolean,
   ) {
-    const fiber = idToArbitraryFiberMap.get(fiberID);
-    if (fiber != null) {
-      // Throw out any pending changes.
-      pendingFiberToErrorsMap.delete(fiber);
-
-      if (fiberIDToMessageCountMap.has(fiberID)) {
-        fiberIDToMessageCountMap.delete(fiberID);
+    const devtoolsInstance = idToDevToolsInstanceMap.get(instanceID);
+    if (devtoolsInstance !== undefined) {
+      let changed = false;
+      if (forError) {
+        if (
+          devtoolsInstance.errors !== null &&
+          devtoolsInstance.errors.size > 0
+        ) {
+          changed = true;
+        }
+        devtoolsInstance.errors = null;
+      } else {
+        if (
+          devtoolsInstance.warnings !== null &&
+          devtoolsInstance.warnings.size > 0
+        ) {
+          changed = true;
+        }
+        devtoolsInstance.warnings = null;
+      }
+      if (devtoolsInstance.kind === FIBER_INSTANCE) {
+        const fiber = devtoolsInstance.data;
+        // Throw out any pending changes.
+        pendingFiberToErrorsMap.delete(fiber);
 
-        // If previous flushed counts have changed, schedule an update too.
-        fibersWithChangedErrorOrWarningCounts.add(fiber);
-        flushPendingEvents();
+        if (changed) {
+          // If previous flushed counts have changed, schedule an update too.
+          fibersWithChangedErrorOrWarningCounts.add(fiber);
+          flushPendingEvents();
 
-        updateMostRecentlyInspectedElementIfNecessary(fiberID);
+          updateMostRecentlyInspectedElementIfNecessary(instanceID);
+        } else {
+          fibersWithChangedErrorOrWarningCounts.delete(fiber);
+        }
       } else {
-        fibersWithChangedErrorOrWarningCounts.delete(fiber);
+        // TODO: Handle VirtualInstance.
       }
     }
   }
 
-  function clearErrorsForElementID(fiberID: number) {
-    clearMessageCountHelper(
-      fiberID,
-      pendingFiberToErrorsMap,
-      fiberIDToErrorsMap,
-    );
+  function clearErrorsForElementID(instanceID: number) {
+    clearMessageCountHelper(instanceID, pendingFiberToErrorsMap, true);
   }
 
-  function clearWarningsForElementID(fiberID: number) {
-    clearMessageCountHelper(
-      fiberID,
-      pendingFiberToWarningsMap,
-      fiberIDToWarningsMap,
-    );
+  function clearWarningsForElementID(instanceID: number) {
+    clearMessageCountHelper(instanceID, pendingFiberToWarningsMap, false);
   }
 
   function updateMostRecentlyInspectedElementIfNecessary(
@@ -845,9 +902,12 @@ export function attach(
     args: $ReadOnlyArray<any>,
   ): void {
     if (type === 'error') {
-      const maybeID = getFiberIDUnsafe(fiber);
+      let fiberInstance = fiberToFiberInstanceMap.get(fiber);
+      if (fiberInstance === undefined && fiber.alternate !== null) {
+        fiberInstance = fiberToFiberInstanceMap.get(fiber.alternate);
+      }
       // if this is an error simulated by us to trigger error boundary, ignore
-      if (maybeID != null && forceErrorForFiberIDs.get(maybeID) === true) {
+      if (fiberInstance !== undefined && fiberInstance.flags & FORCE_ERROR) {
         return;
       }
     }
@@ -1186,39 +1246,24 @@ export function attach(
   // Returns the unique ID for a Fiber or generates and caches a new one if the Fiber hasn't been seen before.
   // Once this method has been called for a Fiber, untrackFiberID() should always be called later to avoid leaking.
   function getOrGenerateFiberID(fiber: Fiber): number {
-    let id = null;
-    if (fiberToIDMap.has(fiber)) {
-      id = fiberToIDMap.get(fiber);
-    } else {
+    let fiberInstance = fiberToFiberInstanceMap.get(fiber);
+    if (fiberInstance === undefined) {
       const {alternate} = fiber;
-      if (alternate !== null && fiberToIDMap.has(alternate)) {
-        id = fiberToIDMap.get(alternate);
+      if (alternate !== null) {
+        fiberInstance = fiberToFiberInstanceMap.get(alternate);
+        if (fiberInstance !== undefined) {
+          // We found the other pair, so we need to make sure we track the other side.
+          fiberToFiberInstanceMap.set(fiber, fiberInstance);
+        }
       }
     }
 
     let didGenerateID = false;
-    if (id === null) {
+    if (fiberInstance === undefined) {
       didGenerateID = true;
-      id = getUID();
-    }
-
-    // This refinement is for Flow purposes only.
-    const refinedID = ((id: any): number);
-
-    // Make sure we're tracking this Fiber
-    // e.g. if it just mounted or an error was logged during initial render.
-    if (!fiberToIDMap.has(fiber)) {
-      fiberToIDMap.set(fiber, refinedID);
-      idToArbitraryFiberMap.set(refinedID, fiber);
-    }
-
-    // Also make sure we're tracking its alternate,
-    // e.g. in case this is the first update after mount.
-    const {alternate} = fiber;
-    if (alternate !== null) {
-      if (!fiberToIDMap.has(alternate)) {
-        fiberToIDMap.set(alternate, refinedID);
-      }
+      fiberInstance = createFiberInstance(fiber);
+      fiberToFiberInstanceMap.set(fiber, fiberInstance);
+      idToDevToolsInstanceMap.set(fiberInstance.id, fiberInstance);
     }
 
     if (__DEBUG__) {
@@ -1232,7 +1277,7 @@ export function attach(
       }
     }
 
-    return refinedID;
+    return fiberInstance.id;
   }
 
   // Returns an ID if one has already been generated for the Fiber or throws.
@@ -1249,12 +1294,16 @@ export function attach(
   // Returns an ID if one has already been generated for the Fiber or null if one has not been generated.
   // Use this method while e.g. logging to avoid over-retaining Fibers.
   function getFiberIDUnsafe(fiber: Fiber): number | null {
-    if (fiberToIDMap.has(fiber)) {
-      return ((fiberToIDMap.get(fiber): any): number);
+    const fiberInstance = fiberToFiberInstanceMap.get(fiber);
+    if (fiberInstance !== undefined) {
+      return fiberInstance.id;
     } else {
       const {alternate} = fiber;
-      if (alternate !== null && fiberToIDMap.has(alternate)) {
-        return ((fiberToIDMap.get(alternate): any): number);
+      if (alternate !== null) {
+        const alternateInstance = fiberToFiberInstanceMap.get(alternate);
+        if (alternateInstance !== undefined) {
+          return alternateInstance.id;
+        }
       }
     }
     return null;
@@ -1306,29 +1355,34 @@ export function attach(
     }
 
     untrackFibersSet.forEach(fiber => {
-      const fiberID = getFiberIDUnsafe(fiber);
-      if (fiberID !== null) {
-        idToArbitraryFiberMap.delete(fiberID);
+      const fiberInstance = fiberToFiberInstanceMap.get(fiber);
+      if (fiberInstance !== undefined) {
+        idToDevToolsInstanceMap.delete(fiberInstance.id);
 
         // Also clear any errors/warnings associated with this fiber.
-        clearErrorsForElementID(fiberID);
-        clearWarningsForElementID(fiberID);
+        clearErrorsForElementID(fiberInstance.id);
+        clearWarningsForElementID(fiberInstance.id);
+        if (fiberInstance.flags & FORCE_ERROR) {
+          fiberInstance.flags &= ~FORCE_ERROR;
+          forceErrorCount--;
+          if (forceErrorCount === 0 && setErrorHandler != null) {
+            setErrorHandler(shouldErrorFiberAlwaysNull);
+          }
+        }
+        if (fiberInstance.flags & FORCE_SUSPENSE_FALLBACK) {
+          fiberInstance.flags &= ~FORCE_SUSPENSE_FALLBACK;
+          forceFallbackCount--;
+          if (forceFallbackCount === 0 && setSuspenseHandler != null) {
+            setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
+          }
+        }
       }
 
-      fiberToIDMap.delete(fiber);
-      fiberToComponentStackMap.delete(fiber);
+      fiberToFiberInstanceMap.delete(fiber);
 
       const {alternate} = fiber;
       if (alternate !== null) {
-        fiberToIDMap.delete(alternate);
-        fiberToComponentStackMap.delete(alternate);
-      }
-
-      if (forceErrorForFiberIDs.has(fiberID)) {
-        forceErrorForFiberIDs.delete(fiberID);
-        if (forceErrorForFiberIDs.size === 0 && setErrorHandler != null) {
-          setErrorHandler(shouldErrorFiberAlwaysNull);
-        }
+        fiberToFiberInstanceMap.delete(alternate);
       }
     });
     untrackFibersSet.clear();
@@ -1738,37 +1792,44 @@ export function attach(
 
   function reevaluateErrorsAndWarnings() {
     fibersWithChangedErrorOrWarningCounts.clear();
-    fiberIDToErrorsMap.forEach((countMap, fiberID) => {
-      const fiber = idToArbitraryFiberMap.get(fiberID);
-      if (fiber != null) {
-        fibersWithChangedErrorOrWarningCounts.add(fiber);
-      }
-    });
-    fiberIDToWarningsMap.forEach((countMap, fiberID) => {
-      const fiber = idToArbitraryFiberMap.get(fiberID);
-      if (fiber != null) {
-        fibersWithChangedErrorOrWarningCounts.add(fiber);
+    // eslint-disable-next-line no-for-of-loops/no-for-of-loops
+    for (const devtoolsInstance of idToDevToolsInstanceMap.values()) {
+      if (devtoolsInstance.kind === FIBER_INSTANCE) {
+        fibersWithChangedErrorOrWarningCounts.add(devtoolsInstance.data);
+      } else {
+        // TODO: Handle VirtualInstance.
       }
-    });
+    }
     recordPendingErrorsAndWarnings();
   }
 
   function mergeMapsAndGetCountHelper(
     fiber: Fiber,
     fiberID: number,
     pendingFiberToMessageCountMap: Map<Fiber, Map<string, number>>,
-    fiberIDToMessageCountMap: Map<number, Map<string, number>>,
+    forError: boolean,
   ): number {
     let newCount = 0;
 
-    let messageCountMap = fiberIDToMessageCountMap.get(fiberID);
+    const devtoolsInstance = idToDevToolsInstanceMap.get(fiberID);
+
+    if (devtoolsInstance === undefined) {
+      return 0;
+    }
+
+    let messageCountMap = forError
+      ? devtoolsInstance.errors
+      : devtoolsInstance.warnings;
 
     const pendingMessageCountMap = pendingFiberToMessageCountMap.get(fiber);
     if (pendingMessageCountMap != null) {
-      if (messageCountMap == null) {
+      if (messageCountMap === null) {
         messageCountMap = pendingMessageCountMap;
-
-        fiberIDToMessageCountMap.set(fiberID, pendingMessageCountMap);
+        if (forError) {
+          devtoolsInstance.errors = pendingMessageCountMap;
+        } else {
+          devtoolsInstance.warnings = pendingMessageCountMap;
+        }
       } else {
         // This Flow refinement should not be necessary and yet...
         const refinedMessageCountMap = ((messageCountMap: any): Map<
@@ -1808,13 +1869,13 @@ export function attach(
           fiber,
           fiberID,
           pendingFiberToErrorsMap,
-          fiberIDToErrorsMap,
+          true,
         );
         const warningCount = mergeMapsAndGetCountHelper(
           fiber,
           fiberID,
           pendingFiberToWarningsMap,
-          fiberIDToWarningsMap,
+          false,
         );
 
         pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS);
@@ -2830,7 +2891,17 @@ export function attach(
 
   function findAllCurrentHostFibers(id: number): $ReadOnlyArray<Fiber> {
     const fibers = [];
-    const fiber = findCurrentFiberUsingSlowPathById(id);
+    const devtoolsInstance = idToDevToolsInstanceMap.get(id);
+    if (devtoolsInstance === undefined) {
+      console.warn(`Could not find DevToolsInstance with id "${id}"`);
+      return fibers;
+    }
+    if (devtoolsInstance.kind !== FIBER_INSTANCE) {
+      // TODO: Handle VirtualInstance.
+      return fibers;
+    }
+    const fiber =
+      findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
     if (!fiber) {
       return fibers;
     }
@@ -2864,7 +2935,17 @@ export function attach(
 
   function findHostInstancesForElementID(id: number) {
     try {
-      const fiber = findCurrentFiberUsingSlowPathById(id);
+      const devtoolsInstance = idToDevToolsInstanceMap.get(id);
+      if (devtoolsInstance === undefined) {
+        console.warn(`Could not find DevToolsInstance with id "${id}"`);
+        return null;
+      }
+      if (devtoolsInstance.kind !== FIBER_INSTANCE) {
+        // TODO: Handle VirtualInstance.
+        return null;
+      }
+      const fiber =
+        findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
       if (fiber === null) {
         return null;
       }
@@ -2878,8 +2959,15 @@ export function attach(
   }
 
   function getDisplayNameForElementID(id: number): null | string {
-    const fiber = idToArbitraryFiberMap.get(id);
-    return fiber != null ? getDisplayNameForFiber(fiber) : null;
+    const devtoolsInstance = idToDevToolsInstanceMap.get(id);
+    if (devtoolsInstance === undefined) {
+      return null;
+    }
+    if (devtoolsInstance.kind === FIBER_INSTANCE) {
+      return getDisplayNameForFiber(devtoolsInstance.data);
+    } else {
+      return devtoolsInstance.data.name || '';
+    }
   }
 
   function getNearestMountedHostInstance(
@@ -2960,13 +3048,10 @@ export function attach(
   // https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberTreeReflection.js
   // It would be nice if we updated React to inject this function directly (vs just indirectly via findDOMNode).
   // BEGIN copied code
-  function findCurrentFiberUsingSlowPathById(id: number): Fiber | null {
-    const fiber = idToArbitraryFiberMap.get(id);
-    if (fiber == null) {
-      console.warn(`Could not find Fiber with id "${id}"`);
-      return null;
-    }
-
+  function findCurrentFiberUsingSlowPathByFiberInstance(
+    fiberInstance: FiberInstance,
+  ): Fiber | null {
+    const fiber = fiberInstance.data;
     const alternate = fiber.alternate;
     if (!alternate) {
       // If there is no alternate, then we only need to check if it is mounted.
@@ -3126,11 +3211,16 @@ export function attach(
   }
 
   function prepareViewElementSource(id: number): void {
-    const fiber = idToArbitraryFiberMap.get(id);
-    if (fiber == null) {
-      console.warn(`Could not find Fiber with id "${id}"`);
+    const devtoolsInstance = idToDevToolsInstanceMap.get(id);
+    if (devtoolsInstance === undefined) {
+      console.warn(`Could not find DevToolsInstance with id "${id}"`);
+      return;
+    }
+    if (devtoolsInstance.kind !== FIBER_INSTANCE) {
+      // TODO: Handle VirtualInstance.
       return;
     }
+    const fiber = devtoolsInstance.data;
 
     const {elementType, tag, type} = fiber;
 
@@ -3168,7 +3258,17 @@ export function attach(
   }
 
   function getOwnersList(id: number): Array<SerializedElement> | null {
-    const fiber = findCurrentFiberUsingSlowPathById(id);
+    const devtoolsInstance = idToDevToolsInstanceMap.get(id);
+    if (devtoolsInstance === undefined) {
+      console.warn(`Could not find DevToolsInstance with id "${id}"`);
+      return null;
+    }
+    if (devtoolsInstance.kind !== FIBER_INSTANCE) {
+      // TODO: Handle VirtualInstance.
+      return null;
+    }
+    const fiber =
+      findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
     if (fiber == null) {
       return null;
     }
@@ -3197,7 +3297,18 @@ export function attach(
     let instance = null;
     let style = null;
 
-    const fiber = findCurrentFiberUsingSlowPathById(id);
+    const devtoolsInstance = idToDevToolsInstanceMap.get(id);
+    if (devtoolsInstance === undefined) {
+      console.warn(`Could not find DevToolsInstance with id "${id}"`);
+      return {instance, style};
+    }
+    if (devtoolsInstance.kind !== FIBER_INSTANCE) {
+      // TODO: Handle VirtualInstance.
+      return {instance, style};
+    }
+
+    const fiber =
+      findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
     if (fiber !== null) {
       instance = fiber.stateNode;
 
@@ -3238,7 +3349,17 @@ export function attach(
   }
 
   function inspectElementRaw(id: number): InspectedElement | null {
-    const fiber = findCurrentFiberUsingSlowPathById(id);
+    const devtoolsInstance = idToDevToolsInstanceMap.get(id);
+    if (devtoolsInstance === undefined) {
+      console.warn(`Could not find DevToolsInstance with id "${id}"`);
+      return null;
+    }
+    if (devtoolsInstance.kind !== FIBER_INSTANCE) {
+      // TODO: Handle VirtualInstance.
+      return null;
+    }
+    const fiber =
+      findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
     if (fiber == null) {
       return null;
     }
@@ -3427,9 +3548,6 @@ export function attach(
       rootType = fiberRoot._debugRootType;
     }
 
-    const errors = fiberIDToErrorsMap.get(id) || new Map();
-    const warnings = fiberIDToWarningsMap.get(id) || new Map();
-
     let isErrored = false;
     let targetErrorBoundaryID;
     if (isErrorBoundary(fiber)) {
@@ -3444,7 +3562,7 @@ export function attach(
       const DidCapture = 0b000000000000000000010000000;
       isErrored =
         (fiber.flags & DidCapture) !== 0 ||
-        forceErrorForFiberIDs.get(id) === true;
+        (devtoolsInstance.flags & FORCE_ERROR) !== 0;
       targetErrorBoundaryID = isErrored ? id : getNearestErrorBoundaryID(fiber);
     } else {
       targetErrorBoundaryID = getNearestErrorBoundaryID(fiber);
@@ -3493,7 +3611,7 @@ export function attach(
         (!isTimedOutSuspense ||
           // If it's showing fallback because we previously forced it to,
           // allow toggling it back to remove the fallback override.
-          forceFallbackForSuspenseIDs.has(id)),
+          (devtoolsInstance.flags & FORCE_SUSPENSE_FALLBACK) !== 0),
 
       // Can view component source location.
       canViewSource,
@@ -3513,8 +3631,14 @@ export function attach(
       hooks,
       props: memoizedProps,
       state: showState ? memoizedState : null,
-      errors: Array.from(errors.entries()),
-      warnings: Array.from(warnings.entries()),
+      errors:
+        devtoolsInstance.errors === null
+          ? []
+          : Array.from(devtoolsInstance.errors.entries()),
+      warnings:
+        devtoolsInstance.warnings === null
+          ? []
+          : Array.from(devtoolsInstance.warnings.entries()),
 
       // List of owners
       owners,
@@ -3612,12 +3736,17 @@ export function attach(
   function updateSelectedElement(inspectedElement: InspectedElement): void {
     const {hooks, id, props} = inspectedElement;
 
-    const fiber = idToArbitraryFiberMap.get(id);
-    if (fiber == null) {
-      console.warn(`Could not find Fiber with id "${id}"`);
+    const devtoolsInstance = idToDevToolsInstanceMap.get(id);
+    if (devtoolsInstance === undefined) {
+      console.warn(`Could not find DevToolsInstance with id "${id}"`);
+      return;
+    }
+    if (devtoolsInstance.kind !== FIBER_INSTANCE) {
+      // TODO: Handle VirtualInstance.
       return;
     }
 
+    const fiber = devtoolsInstance.data;
     const {elementType, stateNode, tag, type} = fiber;
 
     switch (tag) {
@@ -3751,9 +3880,7 @@ export function attach(
         // Log error & cause for user to debug
         console.error(message + '\n\n', error);
         if (error.cause != null) {
-          const fiber = findCurrentFiberUsingSlowPathById(id);
-          const componentName =
-            fiber != null ? getDisplayNameForFiber(fiber) : null;
+          const componentName = getDisplayNameForElementID(id);
           console.error(
             'React DevTools encountered an error while trying to inspect hooks. ' +
               'This is most likely caused by an error in current inspected component' +
@@ -3855,7 +3982,7 @@ export function attach(
       ? mostRecentlyInspectedElement
       : inspectElementRaw(id);
     if (result === null) {
-      console.warn(`Could not find Fiber with id "${id}"`);
+      console.warn(`Could not find DevToolsInstance with id "${id}"`);
       return;
     }
 
@@ -3896,7 +4023,17 @@ export function attach(
     hookID: ?number,
     path: Array<string | number>,
   ): void {
-    const fiber = findCurrentFiberUsingSlowPathById(id);
+    const devtoolsInstance = idToDevToolsInstanceMap.get(id);
+    if (devtoolsInstance === undefined) {
+      console.warn(`Could not find DevToolsInstance with id "${id}"`);
+      return;
+    }
+    if (devtoolsInstance.kind !== FIBER_INSTANCE) {
+      // TODO: Handle VirtualInstance.
+      return;
+    }
+    const fiber =
+      findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
     if (fiber !== null) {
       const instance = fiber.stateNode;
 
@@ -3952,7 +4089,17 @@ export function attach(
     oldPath: Array<string | number>,
     newPath: Array<string | number>,
   ): void {
-    const fiber = findCurrentFiberUsingSlowPathById(id);
+    const devtoolsInstance = idToDevToolsInstanceMap.get(id);
+    if (devtoolsInstance === undefined) {
+      console.warn(`Could not find DevToolsInstance with id "${id}"`);
+      return;
+    }
+    if (devtoolsInstance.kind !== FIBER_INSTANCE) {
+      // TODO: Handle VirtualInstance.
+      return;
+    }
+    const fiber =
+      findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
     if (fiber !== null) {
       const instance = fiber.stateNode;
 
@@ -4018,7 +4165,17 @@ export function attach(
     path: Array<string | number>,
     value: any,
   ): void {
-    const fiber = findCurrentFiberUsingSlowPathById(id);
+    const devtoolsInstance = idToDevToolsInstanceMap.get(id);
+    if (devtoolsInstance === undefined) {
+      console.warn(`Could not find DevToolsInstance with id "${id}"`);
+      return;
+    }
+    if (devtoolsInstance.kind !== FIBER_INSTANCE) {
+      // TODO: Handle VirtualInstance.
+      return;
+    }
+    const fiber =
+      findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
     if (fiber !== null) {
       const instance = fiber.stateNode;
 
@@ -4281,44 +4438,45 @@ export function attach(
     return null;
   }
 
-  // Map of id and its force error status: true (error), false (toggled off),
-  // null (do nothing)
-  const forceErrorForFiberIDs = new Map<number | null, $FlowFixMe>();
+  let forceErrorCount = 0;
 
-  function shouldErrorFiberAccordingToMap(fiber: any) {
+  function shouldErrorFiberAccordingToMap(fiber: any): null | boolean {
     if (typeof setErrorHandler !== 'function') {
       throw new Error(
         'Expected overrideError() to not get called for earlier React versions.',
       );
     }
 
-    const id = getFiberIDUnsafe(fiber);
-    if (id === null) {
+    let fiberInstance = fiberToFiberInstanceMap.get(fiber);
+    if (fiberInstance === undefined && fiber.alternate !== null) {
+      fiberInstance = fiberToFiberInstanceMap.get(fiber.alternate);
+    }
+    if (fiberInstance === undefined) {
       return null;
     }
 
-    let status = null;
-    if (forceErrorForFiberIDs.has(id)) {
-      status = forceErrorForFiberIDs.get(id);
-      if (status === false) {
-        // TRICKY overrideError adds entries to this Map,
-        // so ideally it would be the method that clears them too,
-        // but that would break the functionality of the feature,
-        // since DevTools needs to tell React to act differently than it normally would
-        // (don't just re-render the failed boundary, but reset its errored state too).
-        // So we can only clear it after telling React to reset the state.
-        // Technically this is premature and we should schedule it for later,
-        // since the render could always fail without committing the updated error boundary,
-        // but since this is a DEV-only feature, the simplicity is worth the trade off.
-        forceErrorForFiberIDs.delete(id);
-
-        if (forceErrorForFiberIDs.size === 0) {
-          // Last override is gone. Switch React back to fast path.
-          setErrorHandler(shouldErrorFiberAlwaysNull);
-        }
+    if (fiberInstance.flags & FORCE_ERROR_RESET) {
+      // TRICKY overrideError adds entries to this Map,
+      // so ideally it would be the method that clears them too,
+      // but that would break the functionality of the feature,
+      // since DevTools needs to tell React to act differently than it normally would
+      // (don't just re-render the failed boundary, but reset its errored state too).
+      // So we can only clear it after telling React to reset the state.
+      // Technically this is premature and we should schedule it for later,
+      // since the render could always fail without committing the updated error boundary,
+      // but since this is a DEV-only feature, the simplicity is worth the trade off.
+      forceErrorCount--;
+      fiberInstance.flags &= ~FORCE_ERROR_RESET;
+      if (forceErrorCount === 0) {
+        // Last override is gone. Switch React back to fast path.
+        setErrorHandler(shouldErrorFiberAlwaysNull);
       }
+      return false;
+    } else if (fiberInstance.flags & FORCE_ERROR) {
+      return true;
+    } else {
+      return null;
     }
-    return status;
   }
 
   function overrideError(id: number, forceError: boolean) {
@@ -4331,28 +4489,43 @@ export function attach(
       );
     }
 
-    forceErrorForFiberIDs.set(id, forceError);
-
-    if (forceErrorForFiberIDs.size === 1) {
-      // First override is added. Switch React to slower path.
-      setErrorHandler(shouldErrorFiberAccordingToMap);
+    const devtoolsInstance = idToDevToolsInstanceMap.get(id);
+    if (devtoolsInstance === undefined) {
+      return;
     }
+    if ((devtoolsInstance.flags & (FORCE_ERROR | FORCE_ERROR_RESET)) === 0) {
+      forceErrorCount++;
+      if (forceErrorCount === 1) {
+        // First override is added. Switch React to slower path.
+        setErrorHandler(shouldErrorFiberAccordingToMap);
+      }
+    }
+    devtoolsInstance.flags &= forceError ? ~FORCE_ERROR_RESET : ~FORCE_ERROR;
+    devtoolsInstance.flags |= forceError ? FORCE_ERROR : FORCE_ERROR_RESET;
 
-    const fiber = idToArbitraryFiberMap.get(id);
-    if (fiber != null) {
+    if (devtoolsInstance.kind === FIBER_INSTANCE) {
+      const fiber = devtoolsInstance.data;
       scheduleUpdate(fiber);
+    } else {
+      // TODO: Handle VirtualInstance.
     }
   }
 
   function shouldSuspendFiberAlwaysFalse() {
     return false;
   }
 
-  const forceFallbackForSuspenseIDs = new Set<number>();
+  let forceFallbackCount = 0;
 
   function shouldSuspendFiberAccordingToSet(fiber: any) {
-    const maybeID = getFiberIDUnsafe(((fiber: any): Fiber));
-    return maybeID !== null && forceFallbackForSuspenseIDs.has(maybeID);
+    let fiberInstance = fiberToFiberInstanceMap.get(fiber);
+    if (fiberInstance === undefined && fiber.alternate !== null) {
+      fiberInstance = fiberToFiberInstanceMap.get(fiber.alternate);
+    }
+    return (
+      fiberInstance !== undefined &&
+      (fiberInstance.flags & FORCE_SUSPENSE_FALLBACK) !== 0
+    );
   }
 
   function overrideSuspense(id: number, forceFallback: boolean) {
@@ -4364,22 +4537,36 @@ export function attach(
         'Expected overrideSuspense() to not get called for earlier React versions.',
       );
     }
+    const devtoolsInstance = idToDevToolsInstanceMap.get(id);
+    if (devtoolsInstance === undefined) {
+      return;
+    }
+
     if (forceFallback) {
-      forceFallbackForSuspenseIDs.add(id);
-      if (forceFallbackForSuspenseIDs.size === 1) {
-        // First override is added. Switch React to slower path.
-        setSuspenseHandler(shouldSuspendFiberAccordingToSet);
+      if ((devtoolsInstance.flags & FORCE_SUSPENSE_FALLBACK) === 0) {
+        devtoolsInstance.flags |= FORCE_SUSPENSE_FALLBACK;
+        forceFallbackCount++;
+        if (forceFallbackCount === 1) {
+          // First override is added. Switch React to slower path.
+          setSuspenseHandler(shouldSuspendFiberAccordingToSet);
+        }
       }
     } else {
-      forceFallbackForSuspenseIDs.delete(id);
-      if (forceFallbackForSuspenseIDs.size === 0) {
-        // Last override is gone. Switch React back to fast path.
-        setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
+      if ((devtoolsInstance.flags & FORCE_SUSPENSE_FALLBACK) !== 0) {
+        devtoolsInstance.flags &= ~FORCE_SUSPENSE_FALLBACK;
+        forceFallbackCount--;
+        if (forceFallbackCount === 0) {
+          // Last override is gone. Switch React back to fast path.
+          setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
+        }
       }
     }
-    const fiber = idToArbitraryFiberMap.get(id);
-    if (fiber != null) {
+
+    if (devtoolsInstance.kind === FIBER_INSTANCE) {
+      const fiber = devtoolsInstance.data;
       scheduleUpdate(fiber);
+    } else {
+      // TODO: Handle VirtualInstance.
     }
   }
 
@@ -4557,10 +4744,16 @@ export function attach(
   // The return path will contain Fibers that are "invisible" to the store
   // because their keys and indexes are important to restoring the selection.
   function getPathForElement(id: number): Array<PathFrame> | null {
-    let fiber: ?Fiber = idToArbitraryFiberMap.get(id);
-    if (fiber == null) {
+    const devtoolsInstance = idToDevToolsInstanceMap.get(id);
+    if (devtoolsInstance === undefined) {
+      return null;
+    }
+    if (devtoolsInstance.kind !== FIBER_INSTANCE) {
+      // TODO: Handle VirtualInstance.
       return null;
     }
+
+    let fiber: null | Fiber = devtoolsInstance.data;
     const keyPath = [];
     while (fiber !== null) {
       // $FlowFixMe[incompatible-call] found when upgrading Flow
@@ -4623,26 +4816,33 @@ export function attach(
   }
 
   function hasElementWithId(id: number): boolean {
-    return idToArbitraryFiberMap.has(id);
+    return idToDevToolsInstanceMap.has(id);
   }
 
   function getComponentStackForFiber(fiber: Fiber): string | null {
-    let componentStack = fiberToComponentStackMap.get(fiber);
-    if (componentStack == null) {
-      const dispatcherRef = getDispatcherRef(renderer);
-      if (dispatcherRef == null) {
-        return null;
-      }
-
-      componentStack = getStackByFiberInDevAndProd(
-        ReactTypeOfWork,
-        fiber,
-        dispatcherRef,
-      );
-      fiberToComponentStackMap.set(fiber, componentStack);
+    // TODO: This should really just take an DevToolsInstance directly.
+    let fiberInstance = fiberToFiberInstanceMap.get(fiber);
+    if (fiberInstance === undefined && fiber.alternate !== null) {
+      fiberInstance = fiberToFiberInstanceMap.get(fiber.alternate);
+    }
+    if (fiberInstance === undefined) {
+      // We're no longer tracking this instance.
+      return null;
+    }
+    if (fiberInstance.componentStack !== null) {
+      // Cached entry.
+      return fiberInstance.componentStack;
+    }
+    const dispatcherRef = getDispatcherRef(renderer);
+    if (dispatcherRef == null) {
+      return null;
     }
 
-    return componentStack;
+    return (fiberInstance.componentStack = getStackByFiberInDevAndProd(
+      ReactTypeOfWork,
+      fiber,
+      dispatcherRef,
+    ));
   }
 
   function getSourceForFiber(fiber: Fiber): Source | null {