diff --git a/packages/react-devtools-extensions/src/main/index.js b/packages/react-devtools-extensions/src/main/index.js index a2758567138c8..49930f9c7ddcf 100644 --- a/packages/react-devtools-extensions/src/main/index.js +++ b/packages/react-devtools-extensions/src/main/index.js @@ -345,8 +345,6 @@ function mountReactDevTools() { createBridgeAndStore(); - setReactSelectionFromBrowser(bridge); - createComponentsPanel(); createProfilerPanel(); } diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 9f4343beabf43..1dfea9d14c866 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -96,6 +96,7 @@ export default class Store extends EventEmitter<{ componentFilters: [], error: [Error], hookSettings: [$ReadOnly], + hostInstanceSelected: [Element['id']], settingsUpdated: [$ReadOnly], mutated: [[Array, Map]], recordChangeDescriptions: [], @@ -190,6 +191,9 @@ export default class Store extends EventEmitter<{ _hookSettings: $ReadOnly | null = null; _shouldShowWarningsAndErrors: boolean = false; + // Only used for browser extension for synchronization with built-in Elements panel. + _lastSelectedHostInstanceElementId: Element['id'] | null = null; + constructor(bridge: FrontendBridge, config?: Config) { super(); @@ -265,6 +269,7 @@ export default class Store extends EventEmitter<{ bridge.addListener('saveToClipboard', this.onSaveToClipboard); bridge.addListener('hookSettings', this.onHookSettings); bridge.addListener('backendInitialized', this.onBackendInitialized); + bridge.addListener('selectElement', this.onHostInstanceSelected); } // This is only used in tests to avoid memory leaks. @@ -481,6 +486,10 @@ export default class Store extends EventEmitter<{ return this._unsupportedRendererVersionDetected; } + get lastSelectedHostInstanceElementId(): Element['id'] | null { + return this._lastSelectedHostInstanceElementId; + } + containsElement(id: number): boolean { return this._idToElement.has(id); } @@ -1431,6 +1440,7 @@ export default class Store extends EventEmitter<{ bridge.removeListener('backendVersion', this.onBridgeBackendVersion); bridge.removeListener('bridgeProtocol', this.onBridgeProtocol); bridge.removeListener('saveToClipboard', this.onSaveToClipboard); + bridge.removeListener('selectElement', this.onHostInstanceSelected); if (this._onBridgeProtocolTimeoutID !== null) { clearTimeout(this._onBridgeProtocolTimeoutID); @@ -1507,6 +1517,16 @@ export default class Store extends EventEmitter<{ this._bridge.send('getHookSettings'); // Warm up cached hook settings }; + onHostInstanceSelected: (elementId: number) => void = elementId => { + if (this._lastSelectedHostInstanceElementId === elementId) { + return; + } + + this._lastSelectedHostInstanceElementId = elementId; + // By the time we emit this, there is no guarantee that TreeContext is rendered. + this.emit('hostInstanceSelected', elementId); + }; + getHookSettings: () => void = () => { if (this._hookSettings != null) { this.emit('hookSettings', this._hookSettings); diff --git a/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js b/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js index 041d122c57d34..1d2ed380b9a1f 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js @@ -39,7 +39,7 @@ import { startTransition, } from 'react'; import {createRegExp} from '../utils'; -import {BridgeContext, StoreContext} from '../context'; +import {StoreContext} from '../context'; import Store from '../../store'; import type {Element} from 'react-devtools-shared/src/frontend/types'; @@ -836,7 +836,6 @@ function TreeContextController({ defaultSelectedElementID, defaultSelectedElementIndex, }: Props): React.Node { - const bridge = useContext(BridgeContext); const store = useContext(StoreContext); const initialRevision = useMemo(() => store.revision, [store]); @@ -899,9 +898,15 @@ function TreeContextController({ numElements: store.numElements, ownerSubtreeLeafElementID: null, selectedElementID: - defaultSelectedElementID == null ? null : defaultSelectedElementID, + defaultSelectedElementID != null + ? defaultSelectedElementID + : store.lastSelectedHostInstanceElementId, selectedElementIndex: - defaultSelectedElementIndex == null ? null : defaultSelectedElementIndex, + defaultSelectedElementIndex != null + ? defaultSelectedElementIndex + : store.lastSelectedHostInstanceElementId + ? store.getIndexOfElementID(store.lastSelectedHostInstanceElementId) + : null, // Search searchIndex: null, @@ -914,7 +919,9 @@ function TreeContextController({ // Inspection element panel inspectedElementID: - defaultInspectedElementID == null ? null : defaultInspectedElementID, + defaultInspectedElementID != null + ? defaultInspectedElementID + : store.lastSelectedHostInstanceElementId, }); const dispatchWrapper = useCallback( @@ -929,11 +936,12 @@ function TreeContextController({ // Listen for host element selections. useEffect(() => { - const handleSelectElement = (id: number) => + const handler = (id: Element['id']) => dispatchWrapper({type: 'SELECT_ELEMENT_BY_ID', payload: id}); - bridge.addListener('selectElement', handleSelectElement); - return () => bridge.removeListener('selectElement', handleSelectElement); - }, [bridge, dispatchWrapper]); + + store.addListener('hostInstanceSelected', handler); + return () => store.removeListener('hostInstanceSelected', handler); + }, [store, dispatchWrapper]); // If a newly-selected search result or inspection selection is inside of a collapsed subtree, auto expand it. // This needs to be a layout effect to avoid temporarily flashing an incorrect selection.