From dadbe575dcf6db810ef3b46f938cff20ee2750ed Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Tue, 13 Jan 2026 19:23:07 +0100 Subject: [PATCH 1/2] [DevTools] Clear element inspection if host element not owned by any renderer is selected --- .../react-devtools-shared/src/backend/agent.js | 14 +++++++++++--- packages/react-devtools-shared/src/bridge.js | 2 +- .../react-devtools-shared/src/devtools/store.js | 4 ++-- .../src/devtools/views/Components/TreeContext.js | 2 +- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index 42fbbc9648a..9dcb348488a 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -940,9 +940,17 @@ export default class Agent extends EventEmitter<{ selectNode(target: HostInstance): void { const match = this.getIDForHostInstance(target); - if (match !== null) { - this._bridge.send('selectElement', match.id); - } + this._bridge.send( + 'selectElement', + match !== null + ? match.id + : // If you click outside a React root in the Elements panel, we want to give + // feedback that no selection is possible so we clear the selection. + // Otherwise clicking outside a React root is indistinguishable from clicking + // a different host node that leads to the same selected React element + // due to Component filters + null, + ); } registerRendererInterface( diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index af98cb82989..bc2669fda51 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -214,7 +214,7 @@ export type BackendEvents = { profilingStatus: [boolean], reloadAppForProfiling: [], saveToClipboard: [string], - selectElement: [number], + selectElement: [number | null], shutdown: [], stopInspectingHost: [boolean], scrollTo: [{left: number, top: number, right: number, bottom: number}], diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 6ddaedb7981..6cf23cd2b16 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -147,7 +147,7 @@ export default class Store extends EventEmitter<{ enableSuspenseTab: [], error: [Error], hookSettings: [$ReadOnly], - hostInstanceSelected: [Element['id']], + hostInstanceSelected: [Element['id'] | null], settingsUpdated: [$ReadOnly], mutated: [ [ @@ -2381,7 +2381,7 @@ export default class Store extends EventEmitter<{ this._bridge.send('getHookSettings'); // Warm up cached hook settings }; - onHostInstanceSelected: (elementId: number) => void = elementId => { + onHostInstanceSelected: (elementId: number | null) => void = elementId => { if (this._lastSelectedHostInstanceElementId === elementId) { return; } 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 2a1477ddb57..86d013d55aa 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js @@ -967,7 +967,7 @@ function TreeContextController({ // Listen for host element selections. useEffect(() => { - const handler = (id: Element['id']) => + const handler = (id: Element['id'] | null) => transitionDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: id}); store.addListener('hostInstanceSelected', handler); From fb80c1918aa6a48aaa7621517dff84bf5dbf7f2d Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Tue, 13 Jan 2026 20:01:37 +0100 Subject: [PATCH 2/2] Sync selection when we switch to Elements pane --- packages/react-devtools-extensions/src/main/index.js | 1 + packages/react-devtools-shared/src/backend/agent.js | 9 +++------ packages/react-devtools-shared/src/devtools/store.js | 9 ++++++++- .../src/devtools/views/Components/TreeContext.js | 3 ++- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/react-devtools-extensions/src/main/index.js b/packages/react-devtools-extensions/src/main/index.js index bce648a5dba..76b320f1a66 100644 --- a/packages/react-devtools-extensions/src/main/index.js +++ b/packages/react-devtools-extensions/src/main/index.js @@ -330,6 +330,7 @@ function createElementsInspectPanel() { inspectedElementPortalContainer = portal.container; if (inspectedElementPortalContainer != null && render) { ensureInitialHTMLIsCleared(inspectedElementPortalContainer); + bridge.send('syncSelectionFromBuiltinElementsPanel'); render(); portal.injectStyles(cloneStyleTags); diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index 9dcb348488a..a7c237be721 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -938,8 +938,8 @@ export default class Agent extends EventEmitter<{ } }; - selectNode(target: HostInstance): void { - const match = this.getIDForHostInstance(target); + selectNode(target: HostInstance | null): void { + const match = target !== null ? this.getIDForHostInstance(target) : null; this._bridge.send( 'selectElement', match !== null @@ -996,10 +996,7 @@ export default class Agent extends EventEmitter<{ syncSelectionFromBuiltinElementsPanel: () => void = () => { const target = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0; - if (target == null) { - return; - } - this.selectNode(target); + this.selectNode(target == null ? null : target); }; shutdown: () => void = () => { diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 6cf23cd2b16..c48d5b42fb6 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -2382,7 +2382,14 @@ export default class Store extends EventEmitter<{ }; onHostInstanceSelected: (elementId: number | null) => void = elementId => { - if (this._lastSelectedHostInstanceElementId === elementId) { + if ( + this._lastSelectedHostInstanceElementId === elementId && + // Force clear selection e.g. when we inspect an element in the Components panel + // and then switch to the browser's Elements panel. + // We wouldn't want to stay on the inspected element if we're inspecting + // an element not owned by React when switching to the browser's Elements panel. + elementId !== null + ) { return; } 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 86d013d55aa..80b1e9dce3e 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js @@ -967,8 +967,9 @@ function TreeContextController({ // Listen for host element selections. useEffect(() => { - const handler = (id: Element['id'] | null) => + const handler = (id: Element['id'] | null) => { transitionDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: id}); + }; store.addListener('hostInstanceSelected', handler); return () => store.removeListener('hostInstanceSelected', handler);