[DevTools] Show Owner Stacks in "rendered by" View (#34130)

This shows the stack trace of the JSX at each level so now you can also
jump to the code location for the JSX callsite. The visual is similar to
the owner stacks with `createTask` except when you click the `<...>` you
jump to the Instance in the Components panel.

<img width="593" height="450" alt="Screenshot 2025-08-08 at 12 19 21 AM"
src="https://github.com/user-attachments/assets/dac35faf-9d99-46ce-8b41-7c6fe24625d2"
/>

I'm not sure it's really necessary to have all the JSX stacks of every
owner. We could just have it for the current component and then the rest
of the owners you could get to if you just click that owner instance.

As a bonus, I also use the JSX callsite as the fallback for the "View
Source" button. This is primarily useful for built-ins like `<div>` and
`<Suspense>` that don't have any implementation to jump to anyway. It's
useful to be able to jump to where a boundary was defined.
This commit is contained in:
Sebastian Markbåge 2025-08-11 11:41:30 -04:00 committed by GitHub
parent 59ef3c4baf
commit 7a934a16b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 82 additions and 23 deletions

View File

@ -64,11 +64,22 @@ async function selectElement(
createTestNameSelector('InspectedElementView-Owners'),
])[0];
if (!ownersList) {
return false;
}
const owners = findAllNodes(ownersList, [
createTestNameSelector('OwnerView'),
]);
return (
title &&
title.innerText.includes(titleText) &&
ownersList &&
ownersList.innerText.includes(ownersListText)
owners &&
owners
.map(node => node.innerText)
.join('\n')
.includes(ownersListText)
);
},
{titleText: displayName, ownersListText: waitForOwnersText}

View File

@ -949,6 +949,7 @@ describe('ProfilingCache', () => {
"hocDisplayNames": null,
"id": 1,
"key": null,
"stack": null,
"type": 11,
},
],

View File

@ -4991,6 +4991,10 @@ export function attach(
id: instance.id,
key: fiber.key,
env: null,
stack:
fiber._debugOwner == null || fiber._debugStack == null
? null
: parseStackTrace(fiber._debugStack, 1),
type: getElementTypeForFiber(fiber),
};
} else {
@ -5000,6 +5004,10 @@ export function attach(
id: instance.id,
key: componentInfo.key == null ? null : componentInfo.key,
env: componentInfo.env == null ? null : componentInfo.env,
stack:
componentInfo.owner == null || componentInfo.debugStack == null
? null
: parseStackTrace(componentInfo.debugStack, 1),
type: ElementTypeVirtual,
};
}
@ -5598,6 +5606,11 @@ export function attach(
source,
stack:
fiber._debugOwner == null || fiber._debugStack == null
? null
: parseStackTrace(fiber._debugStack, 1),
// Does the component have legacy context attached to it.
hasLegacyContext,
@ -5698,6 +5711,11 @@ export function attach(
source,
stack:
componentInfo.owner == null || componentInfo.debugStack == null
? null
: parseStackTrace(componentInfo.debugStack, 1),
// Does the component have legacy context attached to it.
hasLegacyContext: false,

View File

@ -796,6 +796,7 @@ export function attach(
id: getID(owner),
key: element.key,
env: null,
stack: null,
type: getElementType(owner),
});
if (owner._currentElement) {
@ -837,6 +838,8 @@ export function attach(
source: null,
stack: null,
// Only legacy context exists in legacy versions.
hasLegacyContext: true,

View File

@ -257,6 +257,7 @@ export type SerializedElement = {
id: number,
key: number | string | null,
env: null | string,
stack: null | ReactStackTrace,
type: ElementType,
};
@ -308,6 +309,9 @@ export type InspectedElement = {
source: ReactFunctionLocation | null,
// The location of the JSX creation.
stack: ReactStackTrace | null,
type: ElementType,
// Meta information about the root this element belongs to.

View File

@ -257,6 +257,7 @@ export function convertInspectedElementBackendToFrontend(
owners,
env,
source,
stack,
context,
hooks,
plugins,
@ -295,6 +296,7 @@ export function convertInspectedElementBackendToFrontend(
// Previous backend implementations (<= 6.1.5) have a different interface for Source.
// This gates the source features for only compatible backends: >= 6.1.6
source: Array.isArray(source) ? source : null,
stack: stack,
type,
owners:
owners === null

View File

@ -51,12 +51,19 @@ export default function InspectedElementWrapper(_: Props): React.Node {
const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
const source =
inspectedElement == null
? null
: inspectedElement.source != null
? inspectedElement.source
: inspectedElement.stack != null && inspectedElement.stack.length > 0
? inspectedElement.stack[0]
: null;
const symbolicatedSourcePromise: null | Promise<ReactFunctionLocation | null> =
React.useMemo(() => {
if (inspectedElement == null) return null;
if (fetchFileWithCaching == null) return Promise.resolve(null);
const {source} = inspectedElement;
if (source == null) return Promise.resolve(null);
const [, sourceURL, line, column] = source;
@ -66,7 +73,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
line,
column,
);
}, [inspectedElement]);
}, [source]);
const element =
inspectedElementID !== null
@ -223,13 +230,12 @@ export default function InspectedElementWrapper(_: Props): React.Node {
{!alwaysOpenInEditor &&
!!editorURL &&
inspectedElement != null &&
inspectedElement.source != null &&
source != null &&
symbolicatedSourcePromise != null && (
<React.Suspense fallback={<Skeleton height={16} width={24} />}>
<OpenInEditorButton
editorURL={editorURL}
source={inspectedElement.source}
source={source}
symbolicatedSourcePromise={symbolicatedSourcePromise}
/>
</React.Suspense>
@ -276,7 +282,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
{!hideViewSourceAction && (
<InspectedElementViewSourceButton
source={inspectedElement ? inspectedElement.source : null}
source={source}
symbolicatedSourcePromise={symbolicatedSourcePromise}
/>
)}

View File

@ -22,6 +22,7 @@ import InspectedElementSuspendedBy from './InspectedElementSuspendedBy';
import NativeStyleEditor from './NativeStyleEditor';
import {enableStyleXFeatures} from 'react-devtools-feature-flags';
import InspectedElementSourcePanel from './InspectedElementSourcePanel';
import StackTraceView from './StackTraceView';
import OwnerView from './OwnerView';
import styles from './InspectedElementView.css';
@ -52,6 +53,7 @@ export default function InspectedElementView({
symbolicatedSourcePromise,
}: Props): React.Node {
const {
stack,
owners,
rendererPackageName,
rendererVersion,
@ -68,8 +70,9 @@ export default function InspectedElementView({
? `${rendererPackageName}@${rendererVersion}`
: null;
const showOwnersList = owners !== null && owners.length > 0;
const showStack = stack != null && stack.length > 0;
const showRenderedBy =
showOwnersList || rendererLabel !== null || rootType !== null;
showStack || showOwnersList || rendererLabel !== null || rootType !== null;
return (
<Fragment>
@ -168,20 +171,26 @@ export default function InspectedElementView({
data-testname="InspectedElementView-Owners">
<div className={styles.OwnersHeader}>rendered by</div>
{showStack ? <StackTraceView stack={stack} /> : null}
{showOwnersList &&
owners?.map(owner => (
<OwnerView
key={owner.id}
displayName={owner.displayName || 'Anonymous'}
hocDisplayNames={owner.hocDisplayNames}
environmentName={
inspectedElement.env === owner.env ? null : owner.env
}
compiledWithForget={owner.compiledWithForget}
id={owner.id}
isInStore={store.containsElement(owner.id)}
type={owner.type}
/>
<>
<OwnerView
key={owner.id}
displayName={owner.displayName || 'Anonymous'}
hocDisplayNames={owner.hocDisplayNames}
environmentName={
inspectedElement.env === owner.env ? null : owner.env
}
compiledWithForget={owner.compiledWithForget}
id={owner.id}
isInStore={store.containsElement(owner.id)}
type={owner.type}
/>
{owner.stack != null && owner.stack.length > 0 ? (
<StackTraceView stack={owner.stack} />
) : null}
</>
))}
{rootType !== null && (

View File

@ -60,7 +60,8 @@ export default function OwnerView({
<span className={styles.OwnerContent}>
<span
className={`${styles.Owner} ${isInStore ? '' : styles.NotInStore}`}
title={displayName}>
title={displayName}
data-testname="OwnerView">
{'<' + displayName + '>'}
</span>

View File

@ -216,6 +216,7 @@ export type SerializedElement = {
id: number,
key: number | string | null,
env: null | string,
stack: null | ReactStackTrace,
hocDisplayNames: Array<string> | null,
compiledWithForget: boolean,
type: ElementType,
@ -279,6 +280,9 @@ export type InspectedElement = {
// Location of component in source code.
source: ReactFunctionLocation | null,
// The location of the JSX creation.
stack: ReactStackTrace | null,
type: ElementType,
// Meta information about the root this element belongs to.