Skip to content

Commit 7a934a1

Browse files
authored
[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.
1 parent 59ef3c4 commit 7a934a1

File tree

10 files changed

+82
-23
lines changed

10 files changed

+82
-23
lines changed

packages/react-devtools-inline/__tests__/__e2e__/devtools-utils.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,22 @@ async function selectElement(
6464
createTestNameSelector('InspectedElementView-Owners'),
6565
])[0];
6666

67+
if (!ownersList) {
68+
return false;
69+
}
70+
71+
const owners = findAllNodes(ownersList, [
72+
createTestNameSelector('OwnerView'),
73+
]);
74+
6775
return (
6876
title &&
6977
title.innerText.includes(titleText) &&
70-
ownersList &&
71-
ownersList.innerText.includes(ownersListText)
78+
owners &&
79+
owners
80+
.map(node => node.innerText)
81+
.join('\n')
82+
.includes(ownersListText)
7283
);
7384
},
7485
{titleText: displayName, ownersListText: waitForOwnersText}

packages/react-devtools-shared/src/__tests__/profilingCache-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,7 @@ describe('ProfilingCache', () => {
949949
"hocDisplayNames": null,
950950
"id": 1,
951951
"key": null,
952+
"stack": null,
952953
"type": 11,
953954
},
954955
],

packages/react-devtools-shared/src/backend/fiber/renderer.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4991,6 +4991,10 @@ export function attach(
49914991
id: instance.id,
49924992
key: fiber.key,
49934993
env: null,
4994+
stack:
4995+
fiber._debugOwner == null || fiber._debugStack == null
4996+
? null
4997+
: parseStackTrace(fiber._debugStack, 1),
49944998
type: getElementTypeForFiber(fiber),
49954999
};
49965000
} else {
@@ -5000,6 +5004,10 @@ export function attach(
50005004
id: instance.id,
50015005
key: componentInfo.key == null ? null : componentInfo.key,
50025006
env: componentInfo.env == null ? null : componentInfo.env,
5007+
stack:
5008+
componentInfo.owner == null || componentInfo.debugStack == null
5009+
? null
5010+
: parseStackTrace(componentInfo.debugStack, 1),
50035011
type: ElementTypeVirtual,
50045012
};
50055013
}
@@ -5598,6 +5606,11 @@ export function attach(
55985606
55995607
source,
56005608
5609+
stack:
5610+
fiber._debugOwner == null || fiber._debugStack == null
5611+
? null
5612+
: parseStackTrace(fiber._debugStack, 1),
5613+
56015614
// Does the component have legacy context attached to it.
56025615
hasLegacyContext,
56035616
@@ -5698,6 +5711,11 @@ export function attach(
56985711
56995712
source,
57005713
5714+
stack:
5715+
componentInfo.owner == null || componentInfo.debugStack == null
5716+
? null
5717+
: parseStackTrace(componentInfo.debugStack, 1),
5718+
57015719
// Does the component have legacy context attached to it.
57025720
hasLegacyContext: false,
57035721

packages/react-devtools-shared/src/backend/legacy/renderer.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,7 @@ export function attach(
796796
id: getID(owner),
797797
key: element.key,
798798
env: null,
799+
stack: null,
799800
type: getElementType(owner),
800801
});
801802
if (owner._currentElement) {
@@ -837,6 +838,8 @@ export function attach(
837838

838839
source: null,
839840

841+
stack: null,
842+
840843
// Only legacy context exists in legacy versions.
841844
hasLegacyContext: true,
842845

packages/react-devtools-shared/src/backend/types.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ export type SerializedElement = {
257257
id: number,
258258
key: number | string | null,
259259
env: null | string,
260+
stack: null | ReactStackTrace,
260261
type: ElementType,
261262
};
262263

@@ -308,6 +309,9 @@ export type InspectedElement = {
308309

309310
source: ReactFunctionLocation | null,
310311

312+
// The location of the JSX creation.
313+
stack: ReactStackTrace | null,
314+
311315
type: ElementType,
312316

313317
// Meta information about the root this element belongs to.

packages/react-devtools-shared/src/backendAPI.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ export function convertInspectedElementBackendToFrontend(
257257
owners,
258258
env,
259259
source,
260+
stack,
260261
context,
261262
hooks,
262263
plugins,
@@ -295,6 +296,7 @@ export function convertInspectedElementBackendToFrontend(
295296
// Previous backend implementations (<= 6.1.5) have a different interface for Source.
296297
// This gates the source features for only compatible backends: >= 6.1.6
297298
source: Array.isArray(source) ? source : null,
299+
stack: stack,
298300
type,
299301
owners:
300302
owners === null

packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,19 @@ export default function InspectedElementWrapper(_: Props): React.Node {
5151

5252
const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
5353

54+
const source =
55+
inspectedElement == null
56+
? null
57+
: inspectedElement.source != null
58+
? inspectedElement.source
59+
: inspectedElement.stack != null && inspectedElement.stack.length > 0
60+
? inspectedElement.stack[0]
61+
: null;
62+
5463
const symbolicatedSourcePromise: null | Promise<ReactFunctionLocation | null> =
5564
React.useMemo(() => {
56-
if (inspectedElement == null) return null;
5765
if (fetchFileWithCaching == null) return Promise.resolve(null);
5866

59-
const {source} = inspectedElement;
6067
if (source == null) return Promise.resolve(null);
6168

6269
const [, sourceURL, line, column] = source;
@@ -66,7 +73,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
6673
line,
6774
column,
6875
);
69-
}, [inspectedElement]);
76+
}, [source]);
7077

7178
const element =
7279
inspectedElementID !== null
@@ -223,13 +230,12 @@ export default function InspectedElementWrapper(_: Props): React.Node {
223230

224231
{!alwaysOpenInEditor &&
225232
!!editorURL &&
226-
inspectedElement != null &&
227-
inspectedElement.source != null &&
233+
source != null &&
228234
symbolicatedSourcePromise != null && (
229235
<React.Suspense fallback={<Skeleton height={16} width={24} />}>
230236
<OpenInEditorButton
231237
editorURL={editorURL}
232-
source={inspectedElement.source}
238+
source={source}
233239
symbolicatedSourcePromise={symbolicatedSourcePromise}
234240
/>
235241
</React.Suspense>
@@ -276,7 +282,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
276282

277283
{!hideViewSourceAction && (
278284
<InspectedElementViewSourceButton
279-
source={inspectedElement ? inspectedElement.source : null}
285+
source={source}
280286
symbolicatedSourcePromise={symbolicatedSourcePromise}
281287
/>
282288
)}

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import InspectedElementSuspendedBy from './InspectedElementSuspendedBy';
2222
import NativeStyleEditor from './NativeStyleEditor';
2323
import {enableStyleXFeatures} from 'react-devtools-feature-flags';
2424
import InspectedElementSourcePanel from './InspectedElementSourcePanel';
25+
import StackTraceView from './StackTraceView';
2526
import OwnerView from './OwnerView';
2627

2728
import styles from './InspectedElementView.css';
@@ -52,6 +53,7 @@ export default function InspectedElementView({
5253
symbolicatedSourcePromise,
5354
}: Props): React.Node {
5455
const {
56+
stack,
5557
owners,
5658
rendererPackageName,
5759
rendererVersion,
@@ -68,8 +70,9 @@ export default function InspectedElementView({
6870
? `${rendererPackageName}@${rendererVersion}`
6971
: null;
7072
const showOwnersList = owners !== null && owners.length > 0;
73+
const showStack = stack != null && stack.length > 0;
7174
const showRenderedBy =
72-
showOwnersList || rendererLabel !== null || rootType !== null;
75+
showStack || showOwnersList || rendererLabel !== null || rootType !== null;
7376

7477
return (
7578
<Fragment>
@@ -168,20 +171,26 @@ export default function InspectedElementView({
168171
data-testname="InspectedElementView-Owners">
169172
<div className={styles.OwnersHeader}>rendered by</div>
170173

174+
{showStack ? <StackTraceView stack={stack} /> : null}
171175
{showOwnersList &&
172176
owners?.map(owner => (
173-
<OwnerView
174-
key={owner.id}
175-
displayName={owner.displayName || 'Anonymous'}
176-
hocDisplayNames={owner.hocDisplayNames}
177-
environmentName={
178-
inspectedElement.env === owner.env ? null : owner.env
179-
}
180-
compiledWithForget={owner.compiledWithForget}
181-
id={owner.id}
182-
isInStore={store.containsElement(owner.id)}
183-
type={owner.type}
184-
/>
177+
<>
178+
<OwnerView
179+
key={owner.id}
180+
displayName={owner.displayName || 'Anonymous'}
181+
hocDisplayNames={owner.hocDisplayNames}
182+
environmentName={
183+
inspectedElement.env === owner.env ? null : owner.env
184+
}
185+
compiledWithForget={owner.compiledWithForget}
186+
id={owner.id}
187+
isInStore={store.containsElement(owner.id)}
188+
type={owner.type}
189+
/>
190+
{owner.stack != null && owner.stack.length > 0 ? (
191+
<StackTraceView stack={owner.stack} />
192+
) : null}
193+
</>
185194
))}
186195

187196
{rootType !== null && (

packages/react-devtools-shared/src/devtools/views/Components/OwnerView.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ export default function OwnerView({
6060
<span className={styles.OwnerContent}>
6161
<span
6262
className={`${styles.Owner} ${isInStore ? '' : styles.NotInStore}`}
63-
title={displayName}>
63+
title={displayName}
64+
data-testname="OwnerView">
6465
{'<' + displayName + '>'}
6566
</span>
6667

packages/react-devtools-shared/src/frontend/types.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ export type SerializedElement = {
216216
id: number,
217217
key: number | string | null,
218218
env: null | string,
219+
stack: null | ReactStackTrace,
219220
hocDisplayNames: Array<string> | null,
220221
compiledWithForget: boolean,
221222
type: ElementType,
@@ -279,6 +280,9 @@ export type InspectedElement = {
279280
// Location of component in source code.
280281
source: ReactFunctionLocation | null,
281282

283+
// The location of the JSX creation.
284+
stack: ReactStackTrace | null,
285+
282286
type: ElementType,
283287

284288
// Meta information about the root this element belongs to.

0 commit comments

Comments
 (0)