Repository: facebook/react
Pull request: DevTools: Reload all roots after Fast Refresh force remount

## Resolves some cases of #21442 as well as #21469 and (I think) #21436

Alternative for #21506 that does not require a React + Refresh bugfix release.

Unlike #21506, this PR works around the corrupted Store state by detecting a broken Fast Refresh remount and forcefully dropping the root and re-mounting the entire tree. This prevents Fibers from getting duplicated in the Store (and in the Components tree). The benefit of this approach is that it doesn't rely on an update or change in behavior to Fast Refresh. (This workaround is pretty dirty, but since it's a DEV-only code path, it's probably okay.)

### Note that this change doesn't fix _all_ of the reported issues (see https://github.com/facebook/react/issues/21442#issuecomment-843090050) but it does fix _some of them_.

---

This PR also slightly refactors the way DevTools assigns and manages unique IDs for Fibers in the backend by removing the indirection of a "primary Fiber" and instead mapping both the primary and alternate.

It also removes the previous cache-on-read behavior of `getFiberID` and splits the method into three separate functions for different use cases:
1. `getOrGenerateFiberID` – Like the previous function, this method returns an ID _or_ generates and caches a new one if the Fiber hasn't been seen before.
2. `getFiberIDUnsafe` – This function returns an ID if one has already been generated _or_ `null` if not. (It can be used to e.g. log a message about a Fiber without potentially causing it to leak.)
3. `getFiberIDThrows` – This function returns an ID if one has already been generated _or_ it throws. (It can be used to guarantee expected behavior rather than to silently cause a leak.)

---

The PR also adds some basic integration tests for Fast Refresh + DevTools, including one that fails without this fix:
![Screen Shot of failed integration test running against main branch](https://user-images.githubusercontent.com/29597/118187024-69a59a80-b40c-11eb-9e87-ccbcb839eefb.png)

Changed files:
filename: packages/react-devtools-shared/src/__tests__/FastRefreshDevToolsIntegration-test.js
patch: @@ -0,0 +1,260 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+describe('Fast Refresh', () => {
+  let React;
+  let ReactDOM;
+  let ReactFreshRuntime;
+  let act;
+  let babel;
+  let container;
+  let exportsObj;
+  let freshPlugin;
+  let store;
+  let withErrorsOrWarningsIgnored;
+
+  afterEach(() => {
+    jest.resetModules();
+  });
+
+  beforeEach(() => {
+    exportsObj = undefined;
+    container = document.createElement('div');
+
+    babel = require('@babel/core');
+    freshPlugin = require('react-refresh/babel');
+
+    store = global.store;
+
+    React = require('react');
+
+    ReactFreshRuntime = require('react-refresh/runtime');
+    ReactFreshRuntime.injectIntoGlobalHook(global);
+
+    ReactDOM = require('react-dom');
+
+    const utils = require('./utils');
+    act = utils.act;
+    withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored;
+  });
+
+  function execute(source) {
+    const compiled = babel.transform(source, {
+      babelrc: false,
+      presets: ['@babel/react'],
+      plugins: [
+        [freshPlugin, {skipEnvCheck: true}],
+        '@babel/plugin-transform-modules-commonjs',
+        '@babel/plugin-transform-destructuring',
+      ].filter(Boolean),
+    }).code;
+    exportsObj = {};
+    // eslint-disable-next-line no-new-func
+    new Function(
+      'global',
+      'React',
+      'exports',
+      '$RefreshReg$',
+      '$RefreshSig$',
+      compiled,
+    )(global, React, exportsObj, $RefreshReg$, $RefreshSig$);
+    // Module systems will register exports as a fallback.
+    // This is useful for cases when e.g. a class is exported,
+    // and we don't want to propagate the update beyond this module.
+    $RefreshReg$(exportsObj.default, 'exports.default');
+    return exportsObj.default;
+  }
+
+  function render(source) {
+    const Component = execute(source);
+    act(() => {
+      ReactDOM.render(<Component />, container);
+    });
+    // Module initialization shouldn't be counted as a hot update.
+    expect(ReactFreshRuntime.performReactRefresh()).toBe(null);
+  }
+
+  function patch(source) {
+    const prevExports = exportsObj;
+    execute(source);
+    const nextExports = exportsObj;
+
+    // Check if exported families have changed.
+    // (In a real module system we'd do this for *all* exports.)
+    // For example, this can happen if you convert a class to a function.
+    // Or if you wrap something in a HOC.
+    const didExportsChange =
+      ReactFreshRuntime.getFamilyByType(prevExports.default) !==
+      ReactFreshRuntime.getFamilyByType(nextExports.default);
+    if (didExportsChange) {
+      // In a real module system, we would propagate such updates upwards,
+      // and re-execute modules that imported this one. (Just like if we edited them.)
+      // This makes adding/removing/renaming exports re-render references to them.
+      // Here, we'll just force a re-render using the newer type to emulate this.
+      const NextComponent = nextExports.default;
+      act(() => {
+        ReactDOM.render(<NextComponent />, container);
+      });
+    }
+    act(() => {
+      const result = ReactFreshRuntime.performReactRefresh();
+      if (!didExportsChange) {
+        // Normally we expect that some components got updated in our tests.
+        expect(result).not.toBe(null);
+      } else {
+        // However, we have tests where we convert functions to classes,
+        // and in those cases it's expected nothing would get updated.
+        // (Instead, the export change branch above would take care of it.)
+      }
+    });
+    expect(ReactFreshRuntime._getMountedRootCount()).toBe(1);
+  }
+
+  function $RefreshReg$(type, id) {
+    ReactFreshRuntime.register(type, id);
+  }
+
+  function $RefreshSig$() {
+    return ReactFreshRuntime.createSignatureFunctionForTransform();
+  }
+
+  it('should not break the DevTools store', () => {
+    render(`
+      function Parent() {
+        return <Child key="A" />;
+      };
+
+      function Child() {
+        return <div />;
+      };
+
+      export default Parent;
+    `);
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <Parent>
+            <Child key="A">
+    `);
+
+    let element = container.firstChild;
+    expect(container.firstChild).not.toBe(null);
+
+    patch(`
+      function Parent() {
+        return <Child key="A" />;
+      };
+
+      function Child() {
+        return <div />;
+      };
+
+      export default Parent;
+    `);
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <Parent>
+            <Child key="A">
+    `);
+
+    // State is preserved; this verifies that Fast Refresh is wired up.
+    expect(container.firstChild).toBe(element);
+    element = container.firstChild;
+
+    patch(`
+      function Parent() {
+        return <Child key="B" />;
+      };
+
+      function Child() {
+        return <div />;
+      };
+
+      export default Parent;
+    `);
+    expect(store).toMatchInlineSnapshot(`
+      [root]
+        ▾ <Parent>
+            <Child key="B">
+    `);
+
+    // State is reset because hooks changed.
+    expect(container.firstChild).not.toBe(element);
+  });
+
+  it('should not break when there are warnings in between patching', () => {
+    withErrorsOrWarningsIgnored(['Expected warning during render'], () => {
+      render(`
+      const {useState} = React;
+
+      export default function Component() {
+        const [state, setState] = useState(1);
+        console.warn("Expected warning during render");
+        return null;
+      }
+    `);
+    });
+    expect(store).toMatchInlineSnapshot(`
+      ✕ 0, ⚠ 1
+      [root]
+          <Component> ⚠
+    `);
+
+    withErrorsOrWarningsIgnored(['Expected warning during render'], () => {
+      patch(`
+      const {useEffect, useState} = React;
+
+      export default function Component() {
+        const [state, setState] = useState(1);
+        console.warn("Expected warning during render");
+        return null;
+      }
+    `);
+    });
+    expect(store).toMatchInlineSnapshot(`
+      ✕ 0, ⚠ 2
+      [root]
+          <Component> ⚠
+    `);
+
+    withErrorsOrWarningsIgnored(['Expected warning during render'], () => {
+      patch(`
+      const {useEffect, useState} = React;
+
+      export default function Component() {
+        const [state, setState] = useState(1);
+        useEffect(() => {});
+        console.warn("Expected warning during render");
+        return null;
+      }
+    `);
+    });
+    expect(store).toMatchInlineSnapshot(`
+      ✕ 0, ⚠ 1
+      [root]
+          <Component> ⚠
+    `);
+
+    withErrorsOrWarningsIgnored(['Expected warning during render'], () => {
+      patch(`
+      const {useEffect, useState} = React;
+
+      export default function Component() {
+        const [state, setState] = useState(1);
+        console.warn("Expected warning during render");
+        return null;
+      }
+    `);
+    });
+    expect(store).toMatchInlineSnapshot(`
+      ✕ 0, ⚠ 1
+      [root]
+          <Component> ⚠
+    `);
+  });
+});

filename: packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap
patch: @@ -10,14 +10,14 @@ Object {
       "props": null,
       "state": null,
     },
-    3 => Object {
+    4 => Object {
       "context": null,
       "didHooksChange": false,
       "isFirstMount": true,
       "props": null,
       "state": null,
     },
-    5 => Object {
+    6 => Object {
       "context": null,
       "didHooksChange": false,
       "isFirstMount": true,
@@ -30,14 +30,14 @@ Object {
   "fiberActualDurations": Map {
     1 => 16,
     2 => 16,
-    3 => 1,
-    5 => 1,
+    4 => 1,
+    6 => 1,
   },
   "fiberSelfDurations": Map {
     1 => 0,
     2 => 10,
-    3 => 1,
-    5 => 1,
+    4 => 1,
+    6 => 1,
   },
   "passiveEffectDuration": null,
   "priorityLevel": "Normal",
@@ -104,7 +104,7 @@ Object {
 exports[`ProfilingCache should calculate self duration correctly for suspended views: CommitDetails with filtered self durations 2`] = `
 Object {
   "changeDescriptions": Map {
-    5 => Object {
+    7 => Object {
       "context": null,
       "didHooksChange": false,
       "isFirstMount": true,
@@ -115,11 +115,11 @@ Object {
   "duration": 3,
   "effectDuration": null,
   "fiberActualDurations": Map {
-    5 => 3,
+    7 => 3,
     3 => 3,
   },
   "fiberSelfDurations": Map {
-    5 => 3,
+    7 => 3,
     3 => 0,
   },
   "passiveEffectDuration": null,
@@ -147,21 +147,21 @@ Object {
       "props": null,
       "state": null,
     },
-    3 => Object {
+    4 => Object {
       "context": null,
       "didHooksChange": false,
       "isFirstMount": true,
       "props": null,
       "state": null,
     },
-    4 => Object {
+    5 => Object {
       "context": null,
       "didHooksChange": false,
       "isFirstMount": true,
       "props": null,
       "state": null,
     },
-    5 => Object {
+    6 => Object {
       "context": null,
       "didHooksChange": false,
       "isFirstMount": true,
@@ -174,16 +174,16 @@ Object {
   "fiberActualDurations": Map {
     1 => 12,
     2 => 12,
-    3 => 0,
-    4 => 1,
+    4 => 0,
     5 => 1,
+    6 => 1,
   },
   "fiberSelfDurations": Map {
     1 => 0,
     2 => 10,
-    3 => 0,
-    4 => 1,
+    4 => 0,
     5 => 1,
+    6 => 1,
   },
   "passiveEffectDuration": null,
   "priorityLevel": "Normal",
@@ -203,21 +203,21 @@ Object {
 exports[`ProfilingCache should collect data for each commit: CommitDetails commitIndex: 1 1`] = `
 Object {
   "changeDescriptions": Map {
-    3 => Object {
+    4 => Object {
       "context": null,
       "didHooksChange": false,
       "isFirstMount": false,
       "props": Array [],
       "state": null,
     },
-    4 => Object {
+    5 => Object {
       "context": null,
       "didHooksChange": false,
       "isFirstMount": false,
       "props": Array [],
       "state": null,
     },
-    6 => Object {
+    7 => Object {
       "context": null,
       "didHooksChange": false,
       "isFirstMount": true,
@@ -237,16 +237,16 @@ Object {
   "duration": 13,
   "effectDuration": null,
   "fiberActualDurations": Map {
-    3 => 0,
-    4 => 1,
-    6 => 2,
+    4 => 0,
+    5 => 1,
+    7 => 2,
     2 => 13,
     1 => 13,
   },
   "fiberSelfDurations": Map {
-    3 => 0,
-    4 => 1,
-    6 => 2,
+    4 => 0,
+    5 => 1,
+    7 => 2,
     2 => 10,
     1 => 0,
   },
@@ -268,7 +268,7 @@ Object {
 exports[`ProfilingCache should collect data for each commit: CommitDetails commitIndex: 2 1`] = `
 Object {
   "changeDescriptions": Map {
-    3 => Object {
+    4 => Object {
       "context": null,
       "didHooksChange": false,
       "isFirstMount": false,
@@ -288,12 +288,12 @@ Object {
   "duration": 10,
   "effectDuration": null,
   "fiberActualDurations": Map {
-    3 => 0,
+    4 => 0,
     2 => 10,
     1 => 10,
   },
   "fiberSelfDurations": Map {
-    3 => 0,
+    4 => 0,
     2 => 10,
     1 => 0,
   },
@@ -368,7 +368,7 @@ Object {
               },
             ],
             Array [
-              3,
+              4,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -378,7 +378,7 @@ Object {
               },
             ],
             Array [
-              4,
+              5,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -388,7 +388,7 @@ Object {
               },
             ],
             Array [
-              5,
+              6,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -410,15 +410,15 @@ Object {
               12,
             ],
             Array [
-              3,
+              4,
               0,
             ],
             Array [
-              4,
+              5,
               1,
             ],
             Array [
-              5,
+              6,
               1,
             ],
           ],
@@ -432,15 +432,15 @@ Object {
               10,
             ],
             Array [
-              3,
+              4,
               0,
             ],
             Array [
-              4,
+              5,
               1,
             ],
             Array [
-              5,
+              6,
               1,
             ],
           ],
@@ -460,7 +460,7 @@ Object {
         Object {
           "changeDescriptions": Array [
             Array [
-              3,
+              4,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -470,7 +470,7 @@ Object {
               },
             ],
             Array [
-              4,
+              5,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -480,7 +480,7 @@ Object {
               },
             ],
             Array [
-              6,
+              7,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -506,15 +506,15 @@ Object {
           "effectDuration": null,
           "fiberActualDurations": Array [
             Array [
-              3,
+              4,
               0,
             ],
             Array [
-              4,
+              5,
               1,
             ],
             Array [
-              6,
+              7,
               2,
             ],
             Array [
@@ -528,15 +528,15 @@ Object {
           ],
           "fiberSelfDurations": Array [
             Array [
-              3,
+              4,
               0,
             ],
             Array [
-              4,
+              5,
               1,
             ],
             Array [
-              6,
+              7,
               2,
             ],
             Array [
@@ -564,7 +564,7 @@ Object {
         Object {
           "changeDescriptions": Array [
             Array [
-              3,
+              4,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -590,7 +590,7 @@ Object {
           "effectDuration": null,
           "fiberActualDurations": Array [
             Array [
-              3,
+              4,
               0,
             ],
             Array [
@@ -604,7 +604,7 @@ Object {
           ],
           "fiberSelfDurations": Array [
             Array [
-              3,
+              4,
               0,
             ],
             Array [
@@ -723,34 +723,34 @@ Object {
           2,
           12000,
           1,
-          3,
+          4,
           5,
           2,
           2,
           2,
           3,
           4,
-          3,
+          4,
           0,
           1,
-          4,
+          5,
           5,
           2,
           2,
           2,
           4,
           4,
-          4,
+          5,
           1000,
           1,
-          5,
+          6,
           8,
           2,
           2,
           2,
           0,
           4,
-          5,
+          6,
           1000,
         ],
         Array [
@@ -766,25 +766,25 @@ Object {
           1,
           50,
           1,
-          6,
+          7,
           5,
           2,
           2,
           1,
           2,
           4,
-          6,
+          7,
           2000,
           4,
           2,
           14000,
           3,
           2,
           4,
-          3,
           4,
-          6,
           5,
+          7,
+          6,
           4,
           1,
           14000,
@@ -795,16 +795,16 @@ Object {
           0,
           2,
           2,
-          6,
-          4,
+          7,
+          5,
           4,
           2,
           11000,
           3,
           2,
           2,
-          3,
-          5,
+          4,
+          6,
           4,
           1,
           11000,
@@ -815,7 +815,7 @@ Object {
           0,
           2,
           1,
-          3,
+          4,
         ],
       ],
       "rootID": 1,
@@ -834,28 +834,28 @@ Array [
 ]
 `;
 
-exports[`ProfilingCache should collect data for each rendered fiber: FiberCommits: element 3 1`] = `
+exports[`ProfilingCache should collect data for each rendered fiber: FiberCommits: element 4 1`] = `
 Array [
   0,
   1,
   2,
 ]
 `;
 
-exports[`ProfilingCache should collect data for each rendered fiber: FiberCommits: element 4 1`] = `
+exports[`ProfilingCache should collect data for each rendered fiber: FiberCommits: element 5 1`] = `
 Array [
   0,
 ]
 `;
 
-exports[`ProfilingCache should collect data for each rendered fiber: FiberCommits: element 5 1`] = `
+exports[`ProfilingCache should collect data for each rendered fiber: FiberCommits: element 6 1`] = `
 Array [
   1,
   2,
 ]
 `;
 
-exports[`ProfilingCache should collect data for each rendered fiber: FiberCommits: element 6 1`] = `
+exports[`ProfilingCache should collect data for each rendered fiber: FiberCommits: element 7 1`] = `
 Array [
   2,
 ]
@@ -879,7 +879,7 @@ Object {
               },
             ],
             Array [
-              3,
+              4,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -889,7 +889,7 @@ Object {
               },
             ],
             Array [
-              4,
+              5,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -911,11 +911,11 @@ Object {
               11,
             ],
             Array [
-              3,
+              4,
               0,
             ],
             Array [
-              4,
+              5,
               1,
             ],
           ],
@@ -929,11 +929,11 @@ Object {
               10,
             ],
             Array [
-              3,
+              4,
               0,
             ],
             Array [
-              4,
+              5,
               1,
             ],
           ],
@@ -953,7 +953,7 @@ Object {
         Object {
           "changeDescriptions": Array [
             Array [
-              3,
+              4,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -963,7 +963,7 @@ Object {
               },
             ],
             Array [
-              5,
+              6,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -989,11 +989,11 @@ Object {
           "effectDuration": null,
           "fiberActualDurations": Array [
             Array [
-              3,
+              4,
               0,
             ],
             Array [
-              5,
+              6,
               1,
             ],
             Array [
@@ -1007,11 +1007,11 @@ Object {
           ],
           "fiberSelfDurations": Array [
             Array [
-              3,
+              4,
               0,
             ],
             Array [
-              5,
+              6,
               1,
             ],
             Array [
@@ -1039,7 +1039,7 @@ Object {
         Object {
           "changeDescriptions": Array [
             Array [
-              3,
+              4,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -1049,7 +1049,7 @@ Object {
               },
             ],
             Array [
-              5,
+              6,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -1059,7 +1059,7 @@ Object {
               },
             ],
             Array [
-              6,
+              7,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -1085,15 +1085,15 @@ Object {
           "effectDuration": null,
           "fiberActualDurations": Array [
             Array [
-              3,
+              4,
               0,
             ],
             Array [
-              5,
+              6,
               1,
             ],
             Array [
-              6,
+              7,
               2,
             ],
             Array [
@@ -1107,15 +1107,15 @@ Object {
           ],
           "fiberSelfDurations": Array [
             Array [
-              3,
+              4,
               0,
             ],
             Array [
-              5,
+              6,
               1,
             ],
             Array [
-              6,
+              7,
               2,
             ],
             Array [
@@ -1182,24 +1182,24 @@ Object {
           2,
           11000,
           1,
-          3,
+          4,
           5,
           2,
           2,
           2,
           3,
           4,
-          3,
+          4,
           0,
           1,
-          4,
+          5,
           8,
           2,
           2,
           2,
           0,
           4,
-          4,
+          5,
           1000,
         ],
         Array [
@@ -1215,24 +1215,24 @@ Object {
           1,
           49,
           1,
-          5,
+          6,
           5,
           2,
           2,
           1,
           2,
           4,
-          5,
+          6,
           1000,
           4,
           2,
           12000,
           3,
           2,
           3,
-          3,
-          5,
           4,
+          6,
+          5,
           4,
           1,
           12000,
@@ -1250,25 +1250,25 @@ Object {
           1,
           50,
           1,
-          6,
+          7,
           5,
           2,
           2,
           1,
           2,
           4,
-          6,
+          7,
           2000,
           4,
           2,
           14000,
           3,
           2,
           4,
-          3,
-          5,
-          6,
           4,
+          6,
+          7,
+          5,
           4,
           1,
           14000,
@@ -1287,21 +1287,21 @@ Object {
   "commitData": Array [
     Object {
       "changeDescriptions": Map {
-        3 => Object {
+        4 => Object {
           "context": null,
           "didHooksChange": false,
           "isFirstMount": false,
           "props": Array [],
           "state": null,
         },
-        4 => Object {
+        5 => Object {
           "context": null,
           "didHooksChange": false,
           "isFirstMount": false,
           "props": Array [],
           "state": null,
         },
-        10 => Object {
+        12 => Object {
           "context": null,
           "didHooksChange": false,
           "isFirstMount": true,
@@ -1321,16 +1321,16 @@ Object {
       "duration": 13,
       "effectDuration": null,
       "fiberActualDurations": Map {
-        3 => 0,
-        4 => 1,
-        10 => 2,
+        4 => 0,
+        5 => 1,
+        12 => 2,
         2 => 13,
         1 => 13,
       },
       "fiberSelfDurations": Map {
-        3 => 0,
-        4 => 1,
-        10 => 2,
+        4 => 0,
+        5 => 1,
+        12 => 2,
         2 => 10,
         1 => 0,
       },
@@ -1349,7 +1349,7 @@ Object {
     },
     Object {
       "changeDescriptions": Map {
-        3 => Object {
+        4 => Object {
           "context": null,
           "didHooksChange": false,
           "isFirstMount": false,
@@ -1369,12 +1369,12 @@ Object {
       "duration": 10,
       "effectDuration": null,
       "fiberActualDurations": Map {
-        3 => 0,
+        4 => 0,
         2 => 10,
         1 => 10,
       },
       "fiberSelfDurations": Map {
-        3 => 0,
+        4 => 0,
         2 => 10,
         1 => 0,
       },
@@ -1431,9 +1431,9 @@ Object {
   "initialTreeBaseDurations": Map {
     1 => 12,
     2 => 12,
-    3 => 0,
-    4 => 1,
+    4 => 0,
     5 => 1,
+    6 => 1,
   },
   "operations": Array [
     Array [
@@ -1449,25 +1449,25 @@ Object {
       1,
       50,
       1,
-      10,
+      12,
       5,
       2,
       2,
       1,
       2,
       4,
-      10,
+      12,
       2000,
       4,
       2,
       14000,
       3,
       2,
       4,
-      3,
       4,
-      10,
       5,
+      12,
+      6,
       4,
       1,
       14000,
@@ -1478,16 +1478,16 @@ Object {
       0,
       2,
       2,
-      10,
-      4,
+      12,
+      5,
       4,
       2,
       11000,
       3,
       2,
       2,
-      3,
-      5,
+      4,
+      6,
       4,
       1,
       11000,
@@ -1498,7 +1498,7 @@ Object {
       0,
       2,
       1,
-      3,
+      4,
     ],
   ],
   "rootID": 1,
@@ -1515,39 +1515,39 @@ Object {
     },
     2 => Object {
       "children": Array [
-        3,
         4,
         5,
+        6,
       ],
       "displayName": "Parent",
       "hocDisplayNames": null,
       "id": 2,
       "key": null,
       "type": 5,
     },
-    3 => Object {
+    4 => Object {
       "children": Array [],
       "displayName": "Child",
       "hocDisplayNames": null,
-      "id": 3,
+      "id": 4,
       "key": "0",
       "type": 5,
     },
-    4 => Object {
+    5 => Object {
       "children": Array [],
       "displayName": "Child",
       "hocDisplayNames": null,
-      "id": 4,
+      "id": 5,
       "key": "1",
       "type": 5,
     },
-    5 => Object {
+    6 => Object {
       "children": Array [],
       "displayName": "Child",
       "hocDisplayNames": Array [
         "Memo",
       ],
-      "id": 5,
+      "id": 6,
       "key": null,
       "type": 8,
     },
@@ -1560,21 +1560,21 @@ Object {
   "commitData": Array [
     Object {
       "changeDescriptions": Map {
-        12 => Object {
+        14 => Object {
           "context": null,
           "didHooksChange": false,
           "isFirstMount": true,
           "props": null,
           "state": null,
         },
-        13 => Object {
+        16 => Object {
           "context": null,
           "didHooksChange": false,
           "isFirstMount": true,
           "props": null,
           "state": null,
         },
-        14 => Object {
+        17 => Object {
           "context": null,
           "didHooksChange": false,
           "isFirstMount": true,
@@ -1585,16 +1585,16 @@ Object {
       "duration": 11,
       "effectDuration": null,
       "fiberActualDurations": Map {
-        11 => 11,
-        12 => 11,
-        13 => 0,
-        14 => 1,
+        13 => 11,
+        14 => 11,
+        16 => 0,
+        17 => 1,
       },
       "fiberSelfDurations": Map {
-        11 => 0,
-        12 => 10,
         13 => 0,
-        14 => 1,
+        14 => 10,
+        16 => 0,
+        17 => 1,
       },
       "passiveEffectDuration": null,
       "priorityLevel": "Normal",
@@ -1603,7 +1603,7 @@ Object {
         Object {
           "displayName": "Anonymous",
           "hocDisplayNames": null,
-          "id": 11,
+          "id": 13,
           "key": null,
           "type": 11,
         },
@@ -1615,7 +1615,7 @@ Object {
   "operations": Array [
     Array [
       1,
-      11,
+      13,
       15,
       6,
       80,
@@ -1633,46 +1633,46 @@ Object {
       1,
       48,
       1,
-      11,
+      13,
       11,
       1,
       1,
       4,
-      11,
+      13,
       11000,
       1,
-      12,
+      14,
       5,
-      11,
+      13,
       0,
       1,
       0,
       4,
-      12,
+      14,
       11000,
       1,
-      13,
+      16,
       5,
-      12,
-      12,
+      14,
+      14,
       2,
       3,
       4,
-      13,
+      16,
       0,
       1,
-      14,
+      17,
       8,
-      12,
-      12,
+      14,
+      14,
       2,
       0,
       4,
-      14,
+      17,
       1000,
     ],
   ],
-  "rootID": 11,
+  "rootID": 13,
   "snapshots": Map {},
 }
 `;
@@ -1693,7 +1693,7 @@ Object {
         Object {
           "displayName": "Anonymous",
           "hocDisplayNames": null,
-          "id": 6,
+          "id": 7,
           "key": null,
           "type": 11,
         },
@@ -1702,62 +1702,62 @@ Object {
   ],
   "displayName": "Parent",
   "initialTreeBaseDurations": Map {
-    6 => 11,
     7 => 11,
-    8 => 0,
-    9 => 1,
+    8 => 11,
+    10 => 0,
+    11 => 1,
   },
   "operations": Array [
     Array [
       1,
-      6,
+      7,
       0,
       2,
       4,
-      9,
+      11,
+      10,
       8,
       7,
-      6,
     ],
   ],
-  "rootID": 6,
+  "rootID": 7,
   "snapshots": Map {
-    6 => Object {
+    7 => Object {
       "children": Array [
-        7,
+        8,
       ],
       "displayName": null,
       "hocDisplayNames": null,
-      "id": 6,
+      "id": 7,
       "key": null,
       "type": 11,
     },
-    7 => Object {
+    8 => Object {
       "children": Array [
-        8,
-        9,
+        10,
+        11,
       ],
       "displayName": "Parent",
       "hocDisplayNames": null,
-      "id": 7,
+      "id": 8,
       "key": null,
       "type": 5,
     },
-    8 => Object {
+    10 => Object {
       "children": Array [],
       "displayName": "Child",
       "hocDisplayNames": null,
-      "id": 8,
+      "id": 10,
       "key": "0",
       "type": 5,
     },
-    9 => Object {
+    11 => Object {
       "children": Array [],
       "displayName": "Child",
       "hocDisplayNames": Array [
         "Memo",
       ],
-      "id": 9,
+      "id": 11,
       "key": null,
       "type": 8,
     },
@@ -1773,7 +1773,7 @@ Object {
         Object {
           "changeDescriptions": Array [
             Array [
-              3,
+              4,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -1783,7 +1783,7 @@ Object {
               },
             ],
             Array [
-              4,
+              5,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -1793,7 +1793,7 @@ Object {
               },
             ],
             Array [
-              10,
+              12,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -1819,15 +1819,15 @@ Object {
           "effectDuration": null,
           "fiberActualDurations": Array [
             Array [
-              3,
+              4,
               0,
             ],
             Array [
-              4,
+              5,
               1,
             ],
             Array [
-              10,
+              12,
               2,
             ],
             Array [
@@ -1841,15 +1841,15 @@ Object {
           ],
           "fiberSelfDurations": Array [
             Array [
-              3,
+              4,
               0,
             ],
             Array [
-              4,
+              5,
               1,
             ],
             Array [
-              10,
+              12,
               2,
             ],
             Array [
@@ -1877,7 +1877,7 @@ Object {
         Object {
           "changeDescriptions": Array [
             Array [
-              3,
+              4,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -1903,7 +1903,7 @@ Object {
           "effectDuration": null,
           "fiberActualDurations": Array [
             Array [
-              3,
+              4,
               0,
             ],
             Array [
@@ -1917,7 +1917,7 @@ Object {
           ],
           "fiberSelfDurations": Array [
             Array [
-              3,
+              4,
               0,
             ],
             Array [
@@ -2004,15 +2004,15 @@ Object {
           12,
         ],
         Array [
-          3,
+          4,
           0,
         ],
         Array [
-          4,
+          5,
           1,
         ],
         Array [
-          5,
+          6,
           1,
         ],
       ],
@@ -2030,25 +2030,25 @@ Object {
           1,
           50,
           1,
-          10,
+          12,
           5,
           2,
           2,
           1,
           2,
           4,
-          10,
+          12,
           2000,
           4,
           2,
           14000,
           3,
           2,
           4,
-          3,
           4,
-          10,
           5,
+          12,
+          6,
           4,
           1,
           14000,
@@ -2059,16 +2059,16 @@ Object {
           0,
           2,
           2,
-          10,
-          4,
+          12,
+          5,
           4,
           2,
           11000,
           3,
           2,
           2,
-          3,
-          5,
+          4,
+          6,
           4,
           1,
           11000,
@@ -2079,7 +2079,7 @@ Object {
           0,
           2,
           1,
-          3,
+          4,
         ],
       ],
       "rootID": 1,
@@ -2101,9 +2101,9 @@ Object {
           2,
           Object {
             "children": Array [
-              3,
               4,
               5,
+              6,
             ],
             "displayName": "Parent",
             "hocDisplayNames": null,
@@ -2113,36 +2113,36 @@ Object {
           },
         ],
         Array [
-          3,
+          4,
           Object {
             "children": Array [],
             "displayName": "Child",
             "hocDisplayNames": null,
-            "id": 3,
+            "id": 4,
             "key": "0",
             "type": 5,
           },
         ],
         Array [
-          4,
+          5,
           Object {
             "children": Array [],
             "displayName": "Child",
             "hocDisplayNames": null,
-            "id": 4,
+            "id": 5,
             "key": "1",
             "type": 5,
           },
         ],
         Array [
-          5,
+          6,
           Object {
             "children": Array [],
             "displayName": "Child",
             "hocDisplayNames": Array [
               "Memo",
             ],
-            "id": 5,
+            "id": 6,
             "key": null,
             "type": 8,
           },
@@ -2154,7 +2154,7 @@ Object {
         Object {
           "changeDescriptions": Array [
             Array [
-              12,
+              14,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -2164,7 +2164,7 @@ Object {
               },
             ],
             Array [
-              13,
+              16,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -2174,7 +2174,7 @@ Object {
               },
             ],
             Array [
-              14,
+              17,
               Object {
                 "context": null,
                 "didHooksChange": false,
@@ -2188,37 +2188,37 @@ Object {
           "effectDuration": null,
           "fiberActualDurations": Array [
             Array [
-              11,
+              13,
               11,
             ],
             Array [
-              12,
+              14,
               11,
             ],
             Array [
-              13,
+              16,
               0,
             ],
             Array [
-              14,
+              17,
               1,
             ],
           ],
           "fiberSelfDurations": Array [
             Array [
-              11,
+              13,
               0,
             ],
             Array [
-              12,
+              14,
               10,
             ],
             Array [
-              13,
+              16,
               0,
             ],
             Array [
-              14,
+              17,
               1,
             ],
           ],
@@ -2229,7 +2229,7 @@ Object {
             Object {
               "displayName": "Anonymous",
               "hocDisplayNames": null,
-              "id": 11,
+              "id": 13,
               "key": null,
               "type": 11,
             },
@@ -2241,7 +2241,7 @@ Object {
       "operations": Array [
         Array [
           1,
-          11,
+          13,
           15,
           6,
           80,
@@ -2259,46 +2259,46 @@ Object {
           1,
           48,
           1,
-          11,
+          13,
           11,
           1,
           1,
           4,
-          11,
+          13,
           11000,
           1,
-          12,
+          14,
           5,
-          11,
+          13,
           0,
           1,
           0,
           4,
-          12,
+          14,
           11000,
           1,
-          13,
+          16,
           5,
-          12,
-          12,
+          14,
+          14,
           2,
           3,
           4,
-          13,
+          16,
           0,
           1,
-          14,
+          17,
           8,
-          12,
-          12,
+          14,
+          14,
           2,
           0,
           4,
-          14,
+          17,
           1000,
         ],
       ],
-      "rootID": 11,
+      "rootID": 13,
       "snapshots": Array [],
     },
     Object {
@@ -2316,7 +2316,7 @@ Object {
             Object {
               "displayName": "Anonymous",
               "hocDisplayNames": null,
-              "id": 6,
+              "id": 7,
               "key": null,
               "type": 11,
             },
@@ -2326,84 +2326,84 @@ Object {
       "displayName": "Parent",
       "initialTreeBaseDurations": Array [
         Array [
-          6,
+          7,
           11,
         ],
         Array [
-          7,
+          8,
           11,
         ],
         Array [
-          8,
+          10,
           0,
         ],
         Array [
-          9,
+          11,
           1,
         ],
       ],
       "operations": Array [
         Array [
           1,
-          6,
+          7,
           0,
           2,
           4,
-          9,
+          11,
+          10,
           8,
           7,
-          6,
         ],
       ],
-      "rootID": 6,
+      "rootID": 7,
       "snapshots": Array [
         Array [
-          6,
+          7,
           Object {
             "children": Array [
-              7,
+              8,
             ],
             "displayName": null,
             "hocDisplayNames": null,
-            "id": 6,
+            "id": 7,
             "key": null,
             "type": 11,
           },
         ],
         Array [
-          7,
+          8,
           Object {
             "children": Array [
-              8,
-              9,
+              10,
+              11,
             ],
             "displayName": "Parent",
             "hocDisplayNames": null,
-            "id": 7,
+            "id": 8,
             "key": null,
             "type": 5,
           },
         ],
         Array [
-          8,
+          10,
           Object {
             "children": Array [],
             "displayName": "Child",
             "hocDisplayNames": null,
-            "id": 8,
+            "id": 10,
             "key": "0",
             "type": 5,
           },
         ],
         Array [
-          9,
+          11,
           Object {
             "children": Array [],
             "displayName": "Child",
             "hocDisplayNames": Array [
               "Memo",
             ],
-            "id": 9,
+            "id": 11,
             "key": null,
             "type": 8,
           },

filename: packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCommitTreeBuilder-test.js.snap
patch: @@ -71,7 +71,7 @@ Object {
     },
     3 => Object {
       "children": Array [
-        4,
+        6,
       ],
       "displayName": "Suspense",
       "hocDisplayNames": null,
@@ -81,11 +81,11 @@ Object {
       "treeBaseDuration": 0,
       "type": 12,
     },
-    4 => Object {
+    6 => Object {
       "children": Array [],
       "displayName": "LazyInnerComponent",
       "hocDisplayNames": null,
-      "id": 4,
+      "id": 6,
       "key": null,
       "parentID": 3,
       "treeBaseDuration": 0,
@@ -167,7 +167,7 @@ Object {
     },
     3 => Object {
       "children": Array [
-        4,
+        6,
       ],
       "displayName": "Suspense",
       "hocDisplayNames": null,
@@ -177,11 +177,11 @@ Object {
       "treeBaseDuration": 0,
       "type": 12,
     },
-    4 => Object {
+    6 => Object {
       "children": Array [],
       "displayName": "LazyInnerComponent",
       "hocDisplayNames": null,
-      "id": 4,
+      "id": 6,
       "key": null,
       "parentID": 3,
       "treeBaseDuration": 0,

filename: packages/react-devtools-shared/src/backend/renderer.js
patch: @@ -631,12 +631,16 @@ export function attach(
 
     // Note that by calling these functions we may be creating the ID for the first time.
     // If the Fiber is then never mounted, we are responsible for cleaning up after ourselves.
-    // This is important because getPrimaryFiber() stores a Fiber in the primaryFibers Set.
-    // If a Fiber never mounts, and we don't clean up after this code, we could leak.
+    // This is important because getOrGenerateFiberID() stores a Fiber in a couple of local Maps.
+    // If the Fiber never mounts and we don't clean up after this code, we could leak.
     // Fortunately we would only leak Fibers that have errors/warnings associated with them,
     // which is hopefully only a small set and only in DEV mode– but this is still not great.
     // We should clean up Fibers like this when flushing; see recordPendingErrorsAndWarnings().
-    const fiberID = getFiberID(getPrimaryFiber(fiber));
+    const fiberID = getOrGenerateFiberID(fiber);
+
+    if (__DEBUG__) {
+      debug('onErrorOrWarning', fiber, null, `${type}: "${message}"`);
+    }
 
     // Mark this Fiber as needed its warning/error count updated during the next flush.
     fibersWithChangedErrorOrWarningCounts.add(fiberID);
@@ -702,19 +706,20 @@ export function attach(
     if (__DEBUG__) {
       const displayName =
         fiber.tag + ':' + (getDisplayNameForFiber(fiber) || 'null');
-      const id = getFiberID(fiber);
+
+      const maybeID = getFiberIDUnsafe(fiber) || '<no id>';
       const parentDisplayName = parentFiber
         ? parentFiber.tag +
           ':' +
           (getDisplayNameForFiber(parentFiber) || 'null')
         : '';
-      const parentID = parentFiber ? getFiberID(parentFiber) : '';
-      // NOTE: calling getFiberID or getPrimaryFiber is unsafe here
-      // because it will put them in the map. For now, we'll omit them.
-      // TODO: better debugging story for this.
+      const maybeParentID = parentFiber
+        ? getFiberIDUnsafe(parentFiber) || '<no-id>'
+        : '';
+
       console.log(
-        `[renderer] %c${name} %c${displayName} (${id}) %c${
-          parentFiber ? `${parentDisplayName} (${parentID})` : ''
+        `[renderer] %c${name} %c${displayName} (${maybeID}) %c${
+          parentFiber ? `${parentDisplayName} (${maybeParentID})` : ''
         } %c${extraString}`,
         'color: red; font-weight: bold;',
         'color: blue;',
@@ -797,9 +802,15 @@ export function attach(
       throw Error('Cannot modify filter preferences while profiling');
     }
 
+    unmountAndRemountAllRoots(() => {
+      applyComponentFilters(componentFilters);
+    });
+  }
+
+  function unmountAndRemountAllRoots(callback?: Function) {
     // Recursively unmount all roots.
     hook.getFiberRoots(rendererID).forEach(root => {
-      currentRootID = getFiberID(getPrimaryFiber(root.current));
+      currentRootID = getOrGenerateFiberID(root.current);
       // The TREE_OPERATION_REMOVE_ROOT operation serves two purposes:
       // 1. It avoids sending unnecessary bridge traffic to clear a root.
       // 2. It preserves Fiber IDs when remounting (below) which in turn ID to error/warning mapping.
@@ -808,14 +819,16 @@ export function attach(
       currentRootID = -1;
     });
 
-    applyComponentFilters(componentFilters);
+    if (typeof callback === 'function') {
+      callback();
+    }
 
     // Reset pseudo counters so that new path selections will be persisted.
     rootDisplayNameCounter.clear();
 
     // Recursively re-mount all roots with new filter criteria applied.
     hook.getFiberRoots(rendererID).forEach(root => {
-      currentRootID = getFiberID(getPrimaryFiber(root.current));
+      currentRootID = getOrGenerateFiberID(root.current);
       setRootPseudoKey(currentRootID, root.current);
       mountFiberRecursively(root.current, null, false, false);
       flushPendingEvents(root);
@@ -946,25 +959,16 @@ export function attach(
     }
   }
 
-  // This is a slightly annoying indirection.
-  // It is currently necessary because DevTools wants to use unique objects as keys for instances.
-  // However fibers have two versions.
-  // We use this set to remember first encountered fiber for each conceptual instance.
-  function getPrimaryFiber(fiber: Fiber): Fiber {
-    if (primaryFibers.has(fiber)) {
-      return fiber;
-    }
-    const {alternate} = fiber;
-    if (alternate != null && primaryFibers.has(alternate)) {
-      return alternate;
-    }
-    primaryFibers.add(fiber);
-    return fiber;
-  }
-
+  // Map of one or more Fibers in a pair to their unique id number.
+  // 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 idToFiberMap: Map<number, Fiber> = new Map();
-  const primaryFibers: Set<Fiber> = new Set();
+
+  // 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();
 
   // When profiling is supported, we store the latest tree base durations for each Fiber.
   // This is so that we can quickly capture a snapshot of those values if profiling starts.
@@ -979,13 +983,84 @@ export function attach(
   // When a mount or update is in progress, this value tracks the root that is being operated on.
   let currentRootID: number = -1;
 
-  function getFiberID(primaryFiber: Fiber): number {
-    if (!fiberToIDMap.has(primaryFiber)) {
-      const id = getUID();
-      fiberToIDMap.set(primaryFiber, id);
-      idToFiberMap.set(id, primaryFiber);
+  // 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 {
+      const {alternate} = fiber;
+      if (alternate !== null && fiberToIDMap.has(alternate)) {
+        id = fiberToIDMap.get(alternate);
+      }
+    }
+
+    if (id === null) {
+      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);
+      }
+    }
+
+    return refinedID;
+  }
+
+  // Returns an ID if one has already been generated for the Fiber or throws.
+  function getFiberIDThrows(fiber: Fiber): number {
+    const maybeID = getFiberIDUnsafe(fiber);
+    if (maybeID !== null) {
+      return maybeID;
+    }
+    throw Error(
+      `Could not find ID for Fiber "${getDisplayNameForFiber(fiber) || ''}"`,
+    );
+  }
+
+  // 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);
+    } else {
+      const {alternate} = fiber;
+      if (alternate !== null && fiberToIDMap.has(alternate)) {
+        return ((fiberToIDMap.get(alternate): any): number);
+      }
+    }
+    return null;
+  }
+
+  // Removes a Fiber (and its alternate) from the Maps used to track their id.
+  // This method should always be called when a Fiber is unmounting.
+  function untrackFiberID(fiber: Fiber) {
+    const fiberID = getFiberIDUnsafe(fiber);
+    if (fiberID !== null) {
+      idToArbitraryFiberMap.delete(fiberID);
+    }
+
+    fiberToIDMap.delete(fiber);
+
+    const {alternate} = fiber;
+    if (alternate !== null) {
+      fiberToIDMap.delete(alternate);
     }
-    return ((fiberToIDMap.get(primaryFiber): any): number);
   }
 
   function getChangeDescription(
@@ -1046,7 +1121,7 @@ export function attach(
     switch (getElementTypeForFiber(fiber)) {
       case ElementTypeClass:
         if (idToContextsMap !== null) {
-          const id = getFiberID(getPrimaryFiber(fiber));
+          const id = getFiberIDThrows(fiber);
           const contexts = getContextsForFiber(fiber);
           if (contexts !== null) {
             idToContextsMap.set(id, contexts);
@@ -1102,7 +1177,7 @@ export function attach(
     switch (getElementTypeForFiber(fiber)) {
       case ElementTypeClass:
         if (idToContextsMap !== null) {
-          const id = getFiberID(getPrimaryFiber(fiber));
+          const id = getFiberIDThrows(fiber);
           const prevContexts = idToContextsMap.has(id)
             ? idToContextsMap.get(id)
             : null;
@@ -1370,15 +1445,13 @@ export function attach(
     clearPendingErrorsAndWarningsAfterDelay();
 
     fibersWithChangedErrorOrWarningCounts.forEach(fiberID => {
-      const fiber = idToFiberMap.get(fiberID);
+      const fiber = idToArbitraryFiberMap.get(fiberID);
       if (fiber != null) {
         // Don't send updates for Fibers that didn't mount due to e.g. Suspense or an error boundary.
         // We may also need to clean up after ourselves to avoid leaks.
         // See inline comments in onErrorOrWarning() for more info.
         if (isFiberMountedImpl(fiber) !== MOUNTED) {
-          fiberToIDMap.delete(fiber);
-          idToFiberMap.delete(fiberID);
-          primaryFibers.delete(fiber);
+          untrackFiberID(fiber);
           return;
         }
 
@@ -1539,7 +1612,7 @@ export function attach(
     }
 
     const isRoot = fiber.tag === HostRoot;
-    const id = getFiberID(getPrimaryFiber(fiber));
+    const id = getOrGenerateFiberID(fiber);
 
     const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner');
     const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
@@ -1562,11 +1635,8 @@ export function attach(
       const elementType = getElementTypeForFiber(fiber);
       const {_debugOwner} = fiber;
 
-      const ownerID =
-        _debugOwner != null ? getFiberID(getPrimaryFiber(_debugOwner)) : 0;
-      const parentID = parentFiber
-        ? getFiberID(getPrimaryFiber(parentFiber))
-        : 0;
+      const ownerID = _debugOwner != null ? getFiberIDThrows(_debugOwner) : 0;
+      const parentID = parentFiber ? getFiberIDThrows(parentFiber) : 0;
 
       const displayNameStringID = getStringID(displayName);
 
@@ -1601,6 +1671,20 @@ export function attach(
       );
     }
 
+    const unsafeID = getFiberIDUnsafe(fiber);
+    if (fiber._debugNeedsRemount) {
+      if (unsafeID === null) {
+        // This inidicates a case we can't recover from:
+        // Fast Refresh has force remounted a component in a way that we don't have an id for.
+        // We could throw but that's a bad user experience.
+        // Or we could ignore the unmount but then Store might end up with a duplicate node.
+        // So a fallback is to completely reset the Store.
+        // This is costly but since Fast Refresh is only used in DEV builds, it should be okay.
+        setTimeout(unmountAndRemountAllRoots, 0);
+        return;
+      }
+    }
+
     if (trackedPathMatchFiber !== null) {
       // We're in the process of trying to restore previous selection.
       // If this fiber matched but is being unmounted, there's no use trying.
@@ -1613,20 +1697,18 @@ export function attach(
       }
     }
 
-    const isRoot = fiber.tag === HostRoot;
-    const primaryFiber = getPrimaryFiber(fiber);
-    if (!fiberToIDMap.has(primaryFiber)) {
+    if (unsafeID === null) {
       // If we've never seen this Fiber, it might be inside of a legacy render Suspense fragment (so the store is not even aware of it).
       // In that case we can just ignore it or it will cause errors later on.
       // One example of this is a Lazy component that never resolves before being unmounted.
       //
       // TODO: This is fragile and can obscure actual bugs.
-      //
-      // Calling getPrimaryFiber() lazily adds fibers to the Map, so clean up after ourselves before returning.
-      primaryFibers.delete(primaryFiber);
       return;
     }
-    const id = getFiberID(primaryFiber);
+
+    // Flow refinement.
+    const id = ((unsafeID: any): number);
+    const isRoot = fiber.tag === HostRoot;
     if (isRoot) {
       // Roots must be removed only after all children (pending and simulated) have been removed.
       // So we track it separately.
@@ -1641,14 +1723,15 @@ export function attach(
         pendingRealUnmountedIDs.push(id);
       }
     }
-    fiberToIDMap.delete(primaryFiber);
-    idToFiberMap.delete(id);
-    primaryFibers.delete(primaryFiber);
 
-    const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
-    if (isProfilingSupported) {
-      idToRootMap.delete(id);
-      idToTreeBaseDurationMap.delete(id);
+    if (!fiber._debugNeedsRemount) {
+      untrackFiberID(fiber);
+
+      const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
+      if (isProfilingSupported) {
+        idToRootMap.delete(id);
+        idToTreeBaseDurationMap.delete(id);
+      }
     }
   }
 
@@ -1675,6 +1758,9 @@ export function attach(
       const shouldIncludeInTree = !shouldFilterFiber(fiber);
       if (shouldIncludeInTree) {
         recordMount(fiber, parentFiber);
+      } else {
+        // Generate an ID even for filtered Fibers, in case it's needed later (e.g. for Profiling).
+        getOrGenerateFiberID(fiber);
       }
 
       if (traceUpdatesEnabled) {
@@ -1785,7 +1871,7 @@ export function attach(
   }
 
   function recordProfilingDurations(fiber: Fiber) {
-    const id = getFiberID(getPrimaryFiber(fiber));
+    const id = getFiberIDThrows(fiber);
     const {actualDuration, treeBaseDuration} = fiber;
 
     idToTreeBaseDurationMap.set(id, treeBaseDuration || 0);
@@ -1873,7 +1959,7 @@ export function attach(
       return;
     }
     pushOperation(TREE_OPERATION_REORDER_CHILDREN);
-    pushOperation(getFiberID(getPrimaryFiber(fiber)));
+    pushOperation(getFiberIDThrows(fiber));
     pushOperation(numChildren);
     for (let i = 0; i < nextChildren.length; i++) {
       pushOperation(nextChildren[i]);
@@ -1885,7 +1971,7 @@ export function attach(
     nextChildren: Array<number>,
   ) {
     if (!shouldFilterFiber(fiber)) {
-      nextChildren.push(getFiberID(getPrimaryFiber(fiber)));
+      nextChildren.push(getFiberIDThrows(fiber));
     } else {
       let child = fiber.child;
       const isTimedOutSuspense =
@@ -1923,6 +2009,8 @@ export function attach(
       debug('updateFiberRecursively()', nextFiber, parentFiber);
     }
 
+    const id = getOrGenerateFiberID(nextFiber);
+
     if (traceUpdatesEnabled) {
       const elementType = getElementTypeForFiber(nextFiber);
       if (traceNearestHostComponentUpdate) {
@@ -1948,8 +2036,7 @@ export function attach(
 
     if (
       mostRecentlyInspectedElement !== null &&
-      mostRecentlyInspectedElement.id ===
-        getFiberID(getPrimaryFiber(nextFiber)) &&
+      mostRecentlyInspectedElement.id === id &&
       didFiberRender(prevFiber, nextFiber)
     ) {
       // If this Fiber has updated, clear cached inspected data.
@@ -2093,7 +2180,7 @@ export function attach(
           // we should fall back to recursively marking the nearest host descendants for highlight.
           if (traceNearestHostComponentUpdate) {
             const hostFibers = findAllCurrentHostFibers(
-              getFiberID(getPrimaryFiber(nextFiber)),
+              getFiberIDThrows(nextFiber),
             );
             hostFibers.forEach(hostFiber => {
               traceUpdatesForNodes.add(hostFiber.stateNode);
@@ -2177,7 +2264,7 @@ export function attach(
       }
       // If we have not been profiling, then we can just walk the tree and build up its current state as-is.
       hook.getFiberRoots(rendererID).forEach(root => {
-        currentRootID = getFiberID(getPrimaryFiber(root.current));
+        currentRootID = getOrGenerateFiberID(root.current);
         setRootPseudoKey(currentRootID, root.current);
 
         // Handle multi-renderer edge-case where only some v16 renderers support profiling.
@@ -2232,7 +2319,7 @@ export function attach(
     const current = root.current;
     const alternate = current.alternate;
 
-    currentRootID = getFiberID(getPrimaryFiber(current));
+    currentRootID = getOrGenerateFiberID(current);
 
     // Before the traversals, remember to start tracking
     // our path in case we have selection to restore.
@@ -2378,7 +2465,7 @@ export function attach(
   }
 
   function getDisplayNameForFiberID(id) {
-    const fiber = idToFiberMap.get(id);
+    const fiber = idToArbitraryFiberMap.get(id);
     return fiber != null ? getDisplayNameForFiber(((fiber: any): Fiber)) : null;
   }
 
@@ -2393,7 +2480,7 @@ export function attach(
           fiber = fiber.return;
         }
       }
-      return getFiberID(getPrimaryFiber(((fiber: any): Fiber)));
+      return getFiberIDThrows(((fiber: any): Fiber));
     }
     return null;
   }
@@ -2463,7 +2550,7 @@ export function attach(
   // 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 = idToFiberMap.get(id);
+    const fiber = idToArbitraryFiberMap.get(id);
     if (fiber == null) {
       console.warn(`Could not find Fiber with id "${id}"`);
       return null;
@@ -2625,7 +2712,7 @@ export function attach(
   }
 
   function prepareViewElementSource(id: number): void {
-    const fiber = idToFiberMap.get(id);
+    const fiber = idToArbitraryFiberMap.get(id);
     if (fiber == null) {
       console.warn(`Could not find Fiber with id "${id}"`);
       return;
@@ -2659,7 +2746,7 @@ export function attach(
   function fiberToSerializedElement(fiber: Fiber): SerializedElement {
     return {
       displayName: getDisplayNameForFiber(fiber) || 'Anonymous',
-      id: getFiberID(getPrimaryFiber(fiber)),
+      id: getFiberIDThrows(fiber),
       key: fiber.key,
       type: getElementTypeForFiber(fiber),
     };
@@ -2990,7 +3077,7 @@ export function attach(
   function updateSelectedElement(inspectedElement: InspectedElement): void {
     const {hooks, id, props} = inspectedElement;
 
-    const fiber = idToFiberMap.get(id);
+    const fiber = idToArbitraryFiberMap.get(id);
     if (fiber == null) {
       console.warn(`Could not find Fiber with id "${id}"`);
       return;
@@ -3508,7 +3595,7 @@ export function attach(
     idToContextsMap = new Map();
 
     hook.getFiberRoots(rendererID).forEach(root => {
-      const rootID = getFiberID(getPrimaryFiber(root.current));
+      const rootID = getFiberIDThrows(root.current);
       ((displayNamesByRootID: any): DisplayNamesByRootID).set(
         rootID,
         getDisplayNameForRoot(root.current),
@@ -3551,8 +3638,8 @@ export function attach(
 
   const forceFallbackForSuspenseIDs = new Set();
   function shouldSuspendFiberAccordingToSet(fiber) {
-    const id = getFiberID(getPrimaryFiber(((fiber: any): Fiber)));
-    return forceFallbackForSuspenseIDs.has(id);
+    const maybeID = getFiberIDUnsafe(((fiber: any): Fiber));
+    return maybeID !== null && forceFallbackForSuspenseIDs.has(maybeID);
   }
 
   function overrideSuspense(id, forceFallback) {
@@ -3577,7 +3664,7 @@ export function attach(
         setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
       }
     }
-    const fiber = idToFiberMap.get(id);
+    const fiber = idToArbitraryFiberMap.get(id);
     if (fiber != null) {
       scheduleUpdate(fiber);
     }
@@ -3728,7 +3815,7 @@ export function attach(
       case HostRoot:
         // Roots don't have a real displayName, index, or key.
         // Instead, we'll use the pseudo key (childDisplayName:indexWithThatName).
-        const id = getFiberID(getPrimaryFiber(fiber));
+        const id = getFiberIDThrows(fiber);
         const pseudoKey = rootPseudoKeys.get(id);
         if (pseudoKey === undefined) {
           throw new Error('Expected mounted root to have known pseudo key.');
@@ -3753,7 +3840,7 @@ 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 = idToFiberMap.get(id);
+    let fiber = idToArbitraryFiberMap.get(id);
     if (fiber == null) {
       return null;
     }
@@ -3784,7 +3871,7 @@ export function attach(
       return null;
     }
     return {
-      id: getFiberID(getPrimaryFiber(fiber)),
+      id: getFiberIDThrows(fiber),
       isFullMatch: trackedPathMatchDepth === trackedPath.length - 1,
     };
   }