From f0c767e2a26ec28d808c22f8af2e09f2e20cdcc2 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Wed, 2 Apr 2025 22:44:38 +0100 Subject: [PATCH] feat[devtools]: display native tag for host components for Native (#32762) Native only. Displays the native tag for Native Host components inside a badge, when user inspects the component. Only displaying will be supported for now, because in order to get native tags indexable, they should be part of the bridge operations, which is technically a breaking change that requires significantly more time investment. The text will only be shown when user hovers over the badge. ![Screenshot 2025-03-26 at 19 46 40](https://github.com/user-attachments/assets/787530cf-c5e5-4b85-8e2a-15b006a3d783) --- .../src/backend/fiber/renderer.js | 30 ++++++++++++++++++ .../src/backend/legacy/renderer.js | 2 ++ .../src/backend/types.js | 3 ++ .../react-devtools-shared/src/backendAPI.js | 2 ++ .../Components/InspectedElementBadges.js | 7 ++++- .../views/Components/InspectedElementView.js | 11 +++++-- .../views/Components/NativeTagBadge.css | 11 +++++++ .../views/Components/NativeTagBadge.js | 31 +++++++++++++++++++ .../src/frontend/types.js | 3 ++ 9 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 packages/react-devtools-shared/src/devtools/views/Components/NativeTagBadge.css create mode 100644 packages/react-devtools-shared/src/devtools/views/Components/NativeTagBadge.js diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index d06a47e9d3..0c1acc99cb 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -808,6 +808,27 @@ function getPublicInstance(instance: HostInstance): HostInstance { return instance; } +function getNativeTag(instance: HostInstance): number | null { + if (typeof instance !== 'object' || instance === null) { + return null; + } + + // Modern. Fabric. + if ( + instance.canonical != null && + typeof instance.canonical.nativeTag === 'number' + ) { + return instance.canonical.nativeTag; + } + + // Legacy. Paper. + if (typeof instance._nativeTag === 'number') { + return instance._nativeTag; + } + + return null; +} + function aquireHostInstance( nearestInstance: DevToolsInstance, hostInstance: HostInstance, @@ -4298,6 +4319,11 @@ export function attach( componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate); } + let nativeTag = null; + if (elementType === ElementTypeHostComponent) { + nativeTag = getNativeTag(fiber.stateNode); + } + return { id: fiberInstance.id, @@ -4364,6 +4390,8 @@ export function attach( rendererVersion: renderer.version, plugins, + + nativeTag, }; } @@ -4457,6 +4485,8 @@ export function attach( rendererVersion: renderer.version, plugins, + + nativeTag: null, }; } diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index 8e26dcae44..cbcc37e319 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -859,6 +859,8 @@ export function attach( plugins: { stylex: null, }, + + nativeTag: null, }; } diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 56fb9a8ec3..f9f11586a0 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -294,6 +294,9 @@ export type InspectedElement = { // UI plugins/visualizations for the inspected element. plugins: Plugins, + + // React Native only. + nativeTag: number | null, }; export const InspectElementErrorType = 'error'; diff --git a/packages/react-devtools-shared/src/backendAPI.js b/packages/react-devtools-shared/src/backendAPI.js index 7a8bb80144..b3668b9d98 100644 --- a/packages/react-devtools-shared/src/backendAPI.js +++ b/packages/react-devtools-shared/src/backendAPI.js @@ -239,6 +239,7 @@ export function convertInspectedElementBackendToFrontend( key, errors, warnings, + nativeTag, } = inspectedElementBackend; const inspectedElement: InspectedElementFrontend = { @@ -273,6 +274,7 @@ export function convertInspectedElementBackendToFrontend( state: hydrateHelper(state), errors, warnings, + nativeTag, }; return inspectedElement; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.js index 9a069b484b..07e303dbc7 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.js @@ -11,21 +11,25 @@ import * as React from 'react'; import Badge from './Badge'; import ForgetBadge from './ForgetBadge'; +import NativeTagBadge from './NativeTagBadge'; import styles from './InspectedElementBadges.css'; type Props = { hocDisplayNames: null | Array, compiledWithForget: boolean, + nativeTag: number | null, }; export default function InspectedElementBadges({ hocDisplayNames, compiledWithForget, + nativeTag, }: Props): React.Node { if ( !compiledWithForget && - (hocDisplayNames == null || hocDisplayNames.length === 0) + (hocDisplayNames == null || hocDisplayNames.length === 0) && + nativeTag === null ) { return null; } @@ -33,6 +37,7 @@ export default function InspectedElementBadges({ return (
{compiledWithForget && } + {nativeTag !== null && } {hocDisplayNames !== null && hocDisplayNames.map(hocDisplayName => ( diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js index 8eec411492..7e18ad5cd7 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js @@ -54,8 +54,14 @@ export default function InspectedElementView({ toggleParseHookNames, symbolicatedSourcePromise, }: Props): React.Node { - const {owners, rendererPackageName, rendererVersion, rootType, source} = - inspectedElement; + const { + owners, + rendererPackageName, + rendererVersion, + rootType, + source, + nativeTag, + } = inspectedElement; const bridge = useContext(BridgeContext); const store = useContext(StoreContext); @@ -75,6 +81,7 @@ export default function InspectedElementView({
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/NativeTagBadge.css b/packages/react-devtools-shared/src/devtools/views/Components/NativeTagBadge.css new file mode 100644 index 0000000000..7188a53743 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/NativeTagBadge.css @@ -0,0 +1,11 @@ +.Toggle { + display: flex; +} + +.Toggle > span { /* targets .ToggleContent */ + padding: 0; +} + +.Badge { + cursor: help; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/NativeTagBadge.js b/packages/react-devtools-shared/src/devtools/views/Components/NativeTagBadge.js new file mode 100644 index 0000000000..118255b536 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/NativeTagBadge.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; + +import Badge from './Badge'; +import Toggle from '../Toggle'; + +import styles from './NativeTagBadge.css'; + +type Props = { + nativeTag: number, +}; + +const noop = () => {}; +const title = + 'Unique identifier for the corresponding native component. React Native only.'; + +export default function NativeTagBadge({nativeTag}: Props): React.Node { + return ( + + Tag {nativeTag} + + ); +} diff --git a/packages/react-devtools-shared/src/frontend/types.js b/packages/react-devtools-shared/src/frontend/types.js index 827ba4dccb..af4aac5503 100644 --- a/packages/react-devtools-shared/src/frontend/types.js +++ b/packages/react-devtools-shared/src/frontend/types.js @@ -259,6 +259,9 @@ export type InspectedElement = { // UI plugins/visualizations for the inspected element. plugins: Plugins, + + // React Native only. + nativeTag: number | null, }; // TODO: Add profiling type