Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/react-devtools-shared/src/__tests__/store-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1546,7 +1546,7 @@ describe('Store', () => {
▸ <Wrapper>
`);

const deepestedNodeID = agent.getIDForHostInstance(ref.current);
const deepestedNodeID = agent.getIDForHostInstance(ref.current).id;

await act(() => store.toggleIsCollapsed(deepestedNodeID, false));
expect(store).toMatchInlineSnapshot(`
Expand Down
100 changes: 34 additions & 66 deletions packages/react-devtools-shared/src/backend/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -455,17 +455,25 @@ export default class Agent extends EventEmitter<{
return renderer.getInstanceAndStyle(id);
}

getIDForHostInstance(target: HostInstance): number | null {
getIDForHostInstance(
target: HostInstance,
onlySuspenseNodes?: boolean,
): null | {id: number, rendererID: number} {
if (isReactNativeEnvironment() || typeof target.nodeType !== 'number') {
// In React Native or non-DOM we simply pick any renderer that has a match.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
try {
const match = renderer.getElementIDForHostInstance(target);
if (match != null) {
return match;
const id = onlySuspenseNodes
? renderer.getSuspenseNodeIDForHostInstance(target)
: renderer.getElementIDForHostInstance(target);
if (id !== null) {
return {
id: id,
rendererID: +rendererID,
};
}
} catch (error) {
// Some old React versions might throw if they can't find a match.
Expand All @@ -478,6 +486,7 @@ export default class Agent extends EventEmitter<{
// that is registered if there isn't an exact match.
let bestMatch: null | Element = null;
let bestRenderer: null | RendererInterface = null;
let bestRendererID: number = 0;
// Find the nearest ancestor which is mounted by a React.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
Expand All @@ -491,19 +500,29 @@ export default class Agent extends EventEmitter<{
// Exact match we can exit early.
bestMatch = nearestNode;
bestRenderer = renderer;
bestRendererID = +rendererID;
break;
}
if (bestMatch === null || bestMatch.contains(nearestNode)) {
// If this is the first match or the previous match contains the new match,
// so the new match is a deeper and therefore better match.
bestMatch = nearestNode;
bestRenderer = renderer;
bestRendererID = +rendererID;
}
}
}
if (bestRenderer != null && bestMatch != null) {
try {
return bestRenderer.getElementIDForHostInstance(bestMatch);
const id = onlySuspenseNodes
? bestRenderer.getSuspenseNodeIDForHostInstance(bestMatch)
: bestRenderer.getElementIDForHostInstance(bestMatch);
if (id !== null) {
return {
id,
rendererID: bestRendererID,
};
}
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
Expand All @@ -514,65 +533,14 @@ export default class Agent extends EventEmitter<{
}

getComponentNameForHostInstance(target: HostInstance): string | null {
// We duplicate this code from getIDForHostInstance to avoid an object allocation.
if (isReactNativeEnvironment() || typeof target.nodeType !== 'number') {
// In React Native or non-DOM we simply pick any renderer that has a match.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
try {
const id = renderer.getElementIDForHostInstance(target);
if (id) {
return renderer.getDisplayNameForElementID(id);
}
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
}
}
return null;
} else {
// In the DOM we use a smarter mechanism to find the deepest a DOM node
// that is registered if there isn't an exact match.
let bestMatch: null | Element = null;
let bestRenderer: null | RendererInterface = null;
// Find the nearest ancestor which is mounted by a React.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
const nearestNode: null | Element = renderer.getNearestMountedDOMNode(
(target: any),
);
if (nearestNode !== null) {
if (nearestNode === target) {
// Exact match we can exit early.
bestMatch = nearestNode;
bestRenderer = renderer;
break;
}
if (bestMatch === null || bestMatch.contains(nearestNode)) {
// If this is the first match or the previous match contains the new match,
// so the new match is a deeper and therefore better match.
bestMatch = nearestNode;
bestRenderer = renderer;
}
}
}
if (bestRenderer != null && bestMatch != null) {
try {
const id = bestRenderer.getElementIDForHostInstance(bestMatch);
if (id) {
return bestRenderer.getDisplayNameForElementID(id);
}
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
}
}
return null;
const match = this.getIDForHostInstance(target);
if (match !== null) {
const renderer = ((this._rendererInterfaces[
(match.rendererID: any)
]: any): RendererInterface);
return renderer.getDisplayNameForElementID(match.id);
}
return null;
}

getBackendVersion: () => void = () => {
Expand Down Expand Up @@ -971,9 +939,9 @@ export default class Agent extends EventEmitter<{
};

selectNode(target: HostInstance): void {
const id = this.getIDForHostInstance(target);
if (id !== null) {
this._bridge.send('selectElement', id);
const match = this.getIDForHostInstance(target);
if (match !== null) {
this._bridge.send('selectElement', match.id);
}
}

Expand Down
46 changes: 45 additions & 1 deletion packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5793,7 +5793,28 @@ export function attach(
return null;
}
if (devtoolsInstance.kind === FIBER_INSTANCE) {
return getDisplayNameForFiber(devtoolsInstance.data);
const fiber = devtoolsInstance.data;
if (fiber.tag === HostRoot) {
// The only reason you'd inspect a HostRoot is to show it as a SuspenseNode.
return 'Initial Paint';
}
if (fiber.tag === SuspenseComponent || fiber.tag === ActivityComponent) {
// For Suspense and Activity components, we can show a better name
// by using the name prop or their owner.
const props = fiber.memoizedProps;
if (props.name != null) {
return props.name;
}
const owner = getUnfilteredOwner(fiber);
if (owner != null) {
if (typeof owner.tag === 'number') {
return getDisplayNameForFiber((owner: any));
} else {
return owner.name || '';
}
}
}
return getDisplayNameForFiber(fiber);
} else {
return devtoolsInstance.data.name || '';
}
Expand Down Expand Up @@ -5834,6 +5855,28 @@ export function attach(
return null;
}

function getSuspenseNodeIDForHostInstance(
publicInstance: HostInstance,
): number | null {
const instance = publicInstanceToDevToolsInstanceMap.get(publicInstance);
if (instance !== undefined) {
// Pick nearest unfiltered SuspenseNode instance.
let suspenseInstance = instance;
while (
suspenseInstance.suspenseNode === null ||
suspenseInstance.kind === FILTERED_FIBER_INSTANCE
) {
if (suspenseInstance.parent === null) {
// We shouldn't get here since we'll always have a suspenseNode at the root.
return null;
}
suspenseInstance = suspenseInstance.parent;
}
return suspenseInstance.id;
}
return null;
}

function getElementAttributeByPath(
id: number,
path: Array<string | number>,
Expand Down Expand Up @@ -8630,6 +8673,7 @@ export function attach(
getDisplayNameForElementID,
getNearestMountedDOMNode,
getElementIDForHostInstance,
getSuspenseNodeIDForHostInstance,
getInstanceAndStyle,
getOwnersList,
getPathForElement,
Expand Down
3 changes: 3 additions & 0 deletions packages/react-devtools-shared/src/backend/flight/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ export function attach(
getElementIDForHostInstance() {
return null;
},
getSuspenseNodeIDForHostInstance() {
return null;
},
getInstanceAndStyle() {
return {
instance: null,
Expand Down
3 changes: 3 additions & 0 deletions packages/react-devtools-shared/src/backend/legacy/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,9 @@ export function attach(
getDisplayNameForElementID,
getNearestMountedDOMNode,
getElementIDForHostInstance,
getSuspenseNodeIDForHostInstance(id: number): null {
return null;
},
getInstanceAndStyle,
findHostInstancesForElementID: (id: number) => {
const hostInstance = findHostInstanceForInternalID(id);
Expand Down
1 change: 1 addition & 0 deletions packages/react-devtools-shared/src/backend/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ export type RendererInterface = {
getComponentStack?: GetComponentStack,
getNearestMountedDOMNode: (component: Element) => Element | null,
getElementIDForHostInstance: GetElementIDForHostInstance,
getSuspenseNodeIDForHostInstance: GetElementIDForHostInstance,
getDisplayNameForElementID: GetDisplayNameForElementID,
getInstanceAndStyle(id: number): InstanceAndStyle,
getProfilingData(): ProfilingDataBackend,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {RendererInterface} from '../../types';
// That is done by the React Native Inspector component.

let iframesListeningTo: Set<HTMLIFrameElement> = new Set();
let inspectOnlySuspenseNodes = false;

export default function setupHighlighter(
bridge: BackendBridge,
Expand All @@ -33,7 +34,8 @@ export default function setupHighlighter(
bridge.addListener('startInspectingHost', startInspectingHost);
bridge.addListener('stopInspectingHost', stopInspectingHost);

function startInspectingHost() {
function startInspectingHost(onlySuspenseNodes: boolean) {
inspectOnlySuspenseNodes = onlySuspenseNodes;
registerListenersOnWindow(window);
}

Expand Down Expand Up @@ -363,9 +365,37 @@ export default function setupHighlighter(
}
}

// Don't pass the name explicitly.
// It will be inferred from DOM tag and Fiber owner.
showOverlay([target], null, agent, false);
if (inspectOnlySuspenseNodes) {
// For Suspense nodes we want to highlight not the actual target but the nodes
// that are the root of the Suspense node.
// TODO: Consider if we should just do the same for other elements because the
// hovered node might just be one child of many in the Component.
const match = agent.getIDForHostInstance(
target,
inspectOnlySuspenseNodes,
);
if (match !== null) {
const renderer = agent.rendererInterfaces[match.rendererID];
if (renderer == null) {
console.warn(
`Invalid renderer id "${match.rendererID}" for element "${match.id}"`,
);
return;
}
highlightHostInstance({
displayName: renderer.getDisplayNameForElementID(match.id),
hideAfterTimeout: false,
id: match.id,
openBuiltinElementsPanel: false,
rendererID: match.rendererID,
scrollIntoView: false,
});
}
} else {
// Don't pass the name explicitly.
// It will be inferred from DOM tag and Fiber owner.
showOverlay([target], null, agent, false);
}
}

function onPointerUp(event: MouseEvent) {
Expand All @@ -374,9 +404,9 @@ export default function setupHighlighter(
}

const selectElementForNode = (node: HTMLElement) => {
const id = agent.getIDForHostInstance(node);
if (id !== null) {
bridge.send('selectElement', id);
const match = agent.getIDForHostInstance(node, inspectOnlySuspenseNodes);
if (match !== null) {
bridge.send('selectElement', match.id);
}
};

Expand Down
2 changes: 1 addition & 1 deletion packages/react-devtools-shared/src/bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ type FrontendEvents = {
savedPreferences: [SavedPreferencesParams],
setTraceUpdatesEnabled: [boolean],
shutdown: [],
startInspectingHost: [],
startInspectingHost: [boolean],
startProfiling: [StartProfilingParams],
stopInspectingHost: [],
scrollToHostInstance: [ScrollToHostInstance],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import Toggle from '../Toggle';
import ButtonIcon from '../ButtonIcon';
import {logEvent} from 'react-devtools-shared/src/Logger';

export default function InspectHostNodesToggle(): React.Node {
export default function InspectHostNodesToggle({
onlySuspenseNodes,
}: {
onlySuspenseNodes?: boolean,
}): React.Node {
const [isInspecting, setIsInspecting] = useState(false);
const bridge = useContext(BridgeContext);

Expand All @@ -24,7 +28,7 @@ export default function InspectHostNodesToggle(): React.Node {

if (isChecked) {
logEvent({event_name: 'inspect-element-button-clicked'});
bridge.send('startInspectingHost');
bridge.send('startInspectingHost', !!onlySuspenseNodes);
} else {
bridge.send('stopInspectingHost');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import {
useLayoutEffect,
useReducer,
useRef,
Fragment,
} from 'react';

import {
localStorageGetItem,
localStorageSetItem,
} from 'react-devtools-shared/src/storage';
import ButtonIcon, {type IconType} from '../ButtonIcon';
import InspectHostNodesToggle from '../Components/InspectHostNodesToggle';
import InspectedElementErrorBoundary from '../Components/InspectedElementErrorBoundary';
import InspectedElement from '../Components/InspectedElement';
import portaledContent from '../portaledContent';
Expand Down Expand Up @@ -156,6 +158,7 @@ function ToggleInspectedElement({
}

function SuspenseTab(_: {}) {
const store = useContext(StoreContext);
const {hideSettings} = useContext(OptionsContext);
const [state, dispatch] = useReducer<LayoutState, null, LayoutAction>(
layoutReducer,
Expand Down Expand Up @@ -367,6 +370,12 @@ function SuspenseTab(_: {}) {
) : (
<ToggleTreeList dispatch={dispatch} state={state} />
)}
{store.supportsClickToInspect && (
<Fragment>
<InspectHostNodesToggle onlySuspenseNodes={true} />
<div className={styles.VRule} />
</Fragment>
)}
<div className={styles.SuspenseBreadcrumbs}>
<SuspenseBreadcrumbs />
</div>
Expand Down
Loading