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.
This commit is contained in:
Ruslan Lesiutin
2025-01-09 18:01:07 +00:00
committed by GitHub
parent d5f3c50f58
commit 54cfa95d3a
3 changed files with 37 additions and 11 deletions

View File

@@ -345,8 +345,6 @@ function mountReactDevTools() {
createBridgeAndStore();
setReactSelectionFromBrowser(bridge);
createComponentsPanel();
createProfilerPanel();
}

View File

@@ -96,6 +96,7 @@ export default class Store extends EventEmitter<{
componentFilters: [],
error: [Error],
hookSettings: [$ReadOnly<DevToolsHookSettings>],
hostInstanceSelected: [Element['id']],
settingsUpdated: [$ReadOnly<DevToolsHookSettings>],
mutated: [[Array<number>, Map<number, number>]],
recordChangeDescriptions: [],
@@ -190,6 +191,9 @@ export default class Store extends EventEmitter<{
_hookSettings: $ReadOnly<DevToolsHookSettings> | null = null;
_shouldShowWarningsAndErrors: boolean = false;
// Only used in 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);

View File

@@ -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.