diff --git a/packages/react-devtools-shared/src/__tests__/FastRefreshDevToolsIntegration-test.js b/packages/react-devtools-shared/src/__tests__/FastRefreshDevToolsIntegration-test.js new file mode 100644 index 0000000000000..4e2cdecfb9e67 --- /dev/null +++ b/packages/react-devtools-shared/src/__tests__/FastRefreshDevToolsIntegration-test.js @@ -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(, 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(, 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 ; + }; + + function Child() { + return
; + }; + + export default Parent; + `); + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + + `); + + let element = container.firstChild; + expect(container.firstChild).not.toBe(null); + + patch(` + function Parent() { + return ; + }; + + function Child() { + return
; + }; + + export default Parent; + `); + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + + `); + + // State is preserved; this verifies that Fast Refresh is wired up. + expect(container.firstChild).toBe(element); + element = container.firstChild; + + patch(` + function Parent() { + return ; + }; + + function Child() { + return
; + }; + + export default Parent; + `); + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + + `); + + // 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] + ⚠ + `); + + 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] + ⚠ + `); + + 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] + ⚠ + `); + + 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] + ⚠ + `); + }); +}); diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap index ad1d487ea790c..8f07adaaf4dae 100644 --- a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap +++ b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap @@ -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,14 +766,14 @@ Object { 1, 50, 1, - 6, + 7, 5, 2, 2, 1, 2, 4, - 6, + 7, 2000, 4, 2, @@ -781,10 +781,10 @@ Object { 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,7 +834,7 @@ 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, @@ -842,20 +842,20 @@ Array [ ] `; -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,14 +1215,14 @@ Object { 1, 49, 1, - 5, + 6, 5, 2, 2, 1, 2, 4, - 5, + 6, 1000, 4, 2, @@ -1230,9 +1230,9 @@ Object { 3, 2, 3, - 3, - 5, 4, + 6, + 5, 4, 1, 12000, @@ -1250,14 +1250,14 @@ Object { 1, 50, 1, - 6, + 7, 5, 2, 2, 1, 2, 4, - 6, + 7, 2000, 4, 2, @@ -1265,10 +1265,10 @@ Object { 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,14 +1449,14 @@ Object { 1, 50, 1, - 10, + 12, 5, 2, 2, 1, 2, 4, - 10, + 12, 2000, 4, 2, @@ -1464,10 +1464,10 @@ Object { 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,9 +1515,9 @@ Object { }, 2 => Object { "children": Array [ - 3, 4, 5, + 6, ], "displayName": "Parent", "hocDisplayNames": null, @@ -1525,29 +1525,29 @@ Object { "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,14 +2030,14 @@ Object { 1, 50, 1, - 10, + 12, 5, 2, 2, 1, 2, 4, - 10, + 12, 2000, 4, 2, @@ -2045,10 +2045,10 @@ Object { 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, }, diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCommitTreeBuilder-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCommitTreeBuilder-test.js.snap index 1fa419013d98f..43f3f91c7a24f 100644 --- a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCommitTreeBuilder-test.js.snap +++ b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCommitTreeBuilder-test.js.snap @@ -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, diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 3afbbf3dd4435..0ef641e410216 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -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) || ''; 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) || '' + : ''; + 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 = new Map(); - const idToFiberMap: Map = new Map(); - const primaryFibers: Set = 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 = 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, ) { 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 | 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, }; }