@samitouri / QOS-React-1 / commits / 54cfa95d3a

DevTools: fix initial host instance selection (#31892)

Related: https://github.com/facebook/react/pull/31342 This fixes RDT behaviour when some DOM element was pre-selected in built-in browser's Elements panel, and then Components panel of React DevTools was opened for the first time. With this change, React DevTools will correctly display the initial state of the Components Tree with the corresponding React Element (if possible) pre-selected. Previously, we would only subscribe listener when `TreeContext` is mounted, but this only happens when user opens one of React DevTools panels for the first time. With this change, we keep state inside `Store`, which is created when Browser DevTools are opened. Later, `TreeContext` will use it for initial state value. Planned next changes: 1. Merge `inspectedElementID` and `selectedElementID`, I have no idea why we need both. 2. Fix issue with `AutoSizer` rendering a blank container.

Ruslan Lesiutin committed Jan 9, 2025 at 18:01 UTC 54cfa95d3a83328868f9fba00d8213e6de6c7d2f
3 files changed +37 -11
packages/react-devtools-extensions/src/main/index.js
-2
@@ -345,8 +345,6 @@ function mountReactDevTools() {
345
346 createBridgeAndStore();
347
348 - setReactSelectionFromBrowser(bridge);
349 -
348 createComponentsPanel();
349 createProfilerPanel();
350 }
packages/react-devtools-shared/src/devtools/store.js
+20
@@ -96,6 +96,7 @@ export default class Store extends EventEmitter<{
96 componentFilters: [],
97 error: [Error],
98 hookSettings: [$ReadOnly<DevToolsHookSettings>],
99 + hostInstanceSelected: [Element['id']],
100 settingsUpdated: [$ReadOnly<DevToolsHookSettings>],
101 mutated: [[Array<number>, Map<number, number>]],
102 recordChangeDescriptions: [],
@@ -190,6 +191,9 @@ export default class Store extends EventEmitter<{
191 _hookSettings: $ReadOnly<DevToolsHookSettings> | null = null;
192 _shouldShowWarningsAndErrors: boolean = false;
193
194 + // Only used in browser extension for synchronization with built-in Elements panel.
195 + _lastSelectedHostInstanceElementId: Element['id'] | null = null;
196 +
197 constructor(bridge: FrontendBridge, config?: Config) {
198 super();
199
@@ -265,6 +269,7 @@ export default class Store extends EventEmitter<{
269 bridge.addListener('saveToClipboard', this.onSaveToClipboard);
270 bridge.addListener('hookSettings', this.onHookSettings);
271 bridge.addListener('backendInitialized', this.onBackendInitialized);
272 + bridge.addListener('selectElement', this.onHostInstanceSelected);
273 }
274
275 // This is only used in tests to avoid memory leaks.
@@ -481,6 +486,10 @@ export default class Store extends EventEmitter<{
486 return this._unsupportedRendererVersionDetected;
487 }
488
489 + get lastSelectedHostInstanceElementId(): Element['id'] | null {
490 + return this._lastSelectedHostInstanceElementId;
491 + }
492 +
493 containsElement(id: number): boolean {
494 return this._idToElement.has(id);
495 }
@@ -1431,6 +1440,7 @@ export default class Store extends EventEmitter<{
1440 bridge.removeListener('backendVersion', this.onBridgeBackendVersion);
1441 bridge.removeListener('bridgeProtocol', this.onBridgeProtocol);
1442 bridge.removeListener('saveToClipboard', this.onSaveToClipboard);
1443 + bridge.removeListener('selectElement', this.onHostInstanceSelected);
1444
1445 if (this._onBridgeProtocolTimeoutID !== null) {
1446 clearTimeout(this._onBridgeProtocolTimeoutID);
@@ -1507,6 +1517,16 @@ export default class Store extends EventEmitter<{
1517 this._bridge.send('getHookSettings'); // Warm up cached hook settings
1518 };
1519
1520 + onHostInstanceSelected: (elementId: number) => void = elementId => {
1521 + if (this._lastSelectedHostInstanceElementId === elementId) {
1522 + return;
1523 + }
1524 +
1525 + this._lastSelectedHostInstanceElementId = elementId;
1526 + // By the time we emit this, there is no guarantee that TreeContext is rendered.
1527 + this.emit('hostInstanceSelected', elementId);
1528 + };
1529 +
1530 getHookSettings: () => void = () => {
1531 if (this._hookSettings != null) {
1532 this.emit('hookSettings', this._hookSettings);
packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js
+17 -9
@@ -39,7 +39,7 @@ import {
39 startTransition,
40 } from 'react';
41 import {createRegExp} from '../utils';
42 -import {BridgeContext, StoreContext} from '../context';
42 +import {StoreContext} from '../context';
43 import Store from '../../store';
44
45 import type {Element} from 'react-devtools-shared/src/frontend/types';
@@ -836,7 +836,6 @@ function TreeContextController({
836 defaultSelectedElementID,
837 defaultSelectedElementIndex,
838 }: Props): React.Node {
839 - const bridge = useContext(BridgeContext);
839 const store = useContext(StoreContext);
840
841 const initialRevision = useMemo(() => store.revision, [store]);
@@ -899,9 +898,15 @@ function TreeContextController({
898 numElements: store.numElements,
899 ownerSubtreeLeafElementID: null,
900 selectedElementID:
902 - defaultSelectedElementID == null ? null : defaultSelectedElementID,
901 + defaultSelectedElementID != null
902 + ? defaultSelectedElementID
903 + : store.lastSelectedHostInstanceElementId,
904 selectedElementIndex:
904 - defaultSelectedElementIndex == null ? null : defaultSelectedElementIndex,
905 + defaultSelectedElementIndex != null
906 + ? defaultSelectedElementIndex
907 + : store.lastSelectedHostInstanceElementId
908 + ? store.getIndexOfElementID(store.lastSelectedHostInstanceElementId)
909 + : null,
910
911 // Search
912 searchIndex: null,
@@ -914,7 +919,9 @@ function TreeContextController({
919
920 // Inspection element panel
921 inspectedElementID:
917 - defaultInspectedElementID == null ? null : defaultInspectedElementID,
922 + defaultInspectedElementID != null
923 + ? defaultInspectedElementID
924 + : store.lastSelectedHostInstanceElementId,
925 });
926
927 const dispatchWrapper = useCallback(
@@ -929,11 +936,12 @@ function TreeContextController({
936
937 // Listen for host element selections.
938 useEffect(() => {
932 - const handleSelectElement = (id: number) =>
939 + const handler = (id: Element['id']) =>
940 dispatchWrapper({type: 'SELECT_ELEMENT_BY_ID', payload: id});
934 - bridge.addListener('selectElement', handleSelectElement);
935 - return () => bridge.removeListener('selectElement', handleSelectElement);
936 - }, [bridge, dispatchWrapper]);
941 +
942 + store.addListener('hostInstanceSelected', handler);
943 + return () => store.removeListener('hostInstanceSelected', handler);
944 + }, [store, dispatchWrapper]);
945
946 // If a newly-selected search result or inspection selection is inside of a collapsed subtree, auto expand it.
947 // This needs to be a layout effect to avoid temporarily flashing an incorrect selection.