@samitouri / QOS-React-2 / commits / 7a934a16b8

[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.

Sebastian Markbåge committed Aug 11, 2025 at 11:41 UTC 7a934a16b861366282e297ee61611f9bd8c524cb
10 files changed +82 -23
packages/react-devtools-inline/__tests__/__e2e__/devtools-utils.js
+13 -2
@@ -64,11 +64,22 @@ async function selectElement(
64 createTestNameSelector('InspectedElementView-Owners'),
65 ])[0];
66
67 + if (!ownersList) {
68 + return false;
69 + }
70 +
71 + const owners = findAllNodes(ownersList, [
72 + createTestNameSelector('OwnerView'),
73 + ]);
74 +
75 return (
76 title &&
77 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)
83 );
84 },
85 {titleText: displayName, ownersListText: waitForOwnersText}
packages/react-devtools-shared/src/__tests__/profilingCache-test.js
+1
@@ -949,6 +949,7 @@ describe('ProfilingCache', () => {
949 "hocDisplayNames": null,
950 "id": 1,
951 "key": null,
952 + "stack": null,
953 "type": 11,
954 },
955 ],
packages/react-devtools-shared/src/backend/fiber/renderer.js
+18
@@ -4991,6 +4991,10 @@ export function attach(
4991 id: instance.id,
4992 key: fiber.key,
4993 env: null,
4994 + stack:
4995 + fiber._debugOwner == null || fiber._debugStack == null
4996 + ? null
4997 + : parseStackTrace(fiber._debugStack, 1),
4998 type: getElementTypeForFiber(fiber),
4999 };
5000 } else {
@@ -5000,6 +5004,10 @@ export function attach(
5004 id: instance.id,
5005 key: componentInfo.key == null ? null : componentInfo.key,
5006 env: componentInfo.env == null ? null : componentInfo.env,
5007 + stack:
5008 + componentInfo.owner == null || componentInfo.debugStack == null
5009 + ? null
5010 + : parseStackTrace(componentInfo.debugStack, 1),
5011 type: ElementTypeVirtual,
5012 };
5013 }
@@ -5598,6 +5606,11 @@ export function attach(
5606
5607 source,
5608
5609 + stack:
5610 + fiber._debugOwner == null || fiber._debugStack == null
5611 + ? null
5612 + : parseStackTrace(fiber._debugStack, 1),
5613 +
5614 // Does the component have legacy context attached to it.
5615 hasLegacyContext,
5616
@@ -5698,6 +5711,11 @@ export function attach(
5711
5712 source,
5713
5714 + stack:
5715 + componentInfo.owner == null || componentInfo.debugStack == null
5716 + ? null
5717 + : parseStackTrace(componentInfo.debugStack, 1),
5718 +
5719 // Does the component have legacy context attached to it.
5720 hasLegacyContext: false,
5721
packages/react-devtools-shared/src/backend/legacy/renderer.js
+3
@@ -796,6 +796,7 @@ export function attach(
796 id: getID(owner),
797 key: element.key,
798 env: null,
799 + stack: null,
800 type: getElementType(owner),
801 });
802 if (owner._currentElement) {
@@ -837,6 +838,8 @@ export function attach(
838
839 source: null,
840
841 + stack: null,
842 +
843 // Only legacy context exists in legacy versions.
844 hasLegacyContext: true,
845
packages/react-devtools-shared/src/backend/types.js
+4
@@ -257,6 +257,7 @@ export type SerializedElement = {
257 id: number,
258 key: number | string | null,
259 env: null | string,
260 + stack: null | ReactStackTrace,
261 type: ElementType,
262 };
263
@@ -308,6 +309,9 @@ export type InspectedElement = {
309
310 source: ReactFunctionLocation | null,
311
312 + // The location of the JSX creation.
313 + stack: ReactStackTrace | null,
314 +
315 type: ElementType,
316
317 // Meta information about the root this element belongs to.
packages/react-devtools-shared/src/backendAPI.js
+2
@@ -257,6 +257,7 @@ export function convertInspectedElementBackendToFrontend(
257 owners,
258 env,
259 source,
260 + stack,
261 context,
262 hooks,
263 plugins,
@@ -295,6 +296,7 @@ export function convertInspectedElementBackendToFrontend(
296 // Previous backend implementations (<= 6.1.5) have a different interface for Source.
297 // This gates the source features for only compatible backends: >= 6.1.6
298 source: Array.isArray(source) ? source : null,
299 + stack: stack,
300 type,
301 owners:
302 owners === null
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
+13 -7
@@ -51,12 +51,19 @@ export default function InspectedElementWrapper(_: Props): React.Node {
51
52 const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
53
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 +
63 const symbolicatedSourcePromise: null | Promise<ReactFunctionLocation | null> =
64 React.useMemo(() => {
56 - if (inspectedElement == null) return null;
65 if (fetchFileWithCaching == null) return Promise.resolve(null);
66
59 - const {source} = inspectedElement;
67 if (source == null) return Promise.resolve(null);
68
69 const [, sourceURL, line, column] = source;
@@ -66,7 +73,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
73 line,
74 column,
75 );
69 - }, [inspectedElement]);
76 + }, [source]);
77
78 const element =
79 inspectedElementID !== null
@@ -223,13 +230,12 @@ export default function InspectedElementWrapper(_: Props): React.Node {
230
231 {!alwaysOpenInEditor &&
232 !!editorURL &&
226 - inspectedElement != null &&
227 - inspectedElement.source != null &&
233 + source != null &&
234 symbolicatedSourcePromise != null && (
235 <React.Suspense fallback={<Skeleton height={16} width={24} />}>
236 <OpenInEditorButton
237 editorURL={editorURL}
232 - source={inspectedElement.source}
238 + source={source}
239 symbolicatedSourcePromise={symbolicatedSourcePromise}
240 />
241 </React.Suspense>
@@ -276,7 +282,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
282
283 {!hideViewSourceAction && (
284 <InspectedElementViewSourceButton
279 - source={inspectedElement ? inspectedElement.source : null}
285 + source={source}
286 symbolicatedSourcePromise={symbolicatedSourcePromise}
287 />
288 )}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
+22 -13
@@ -22,6 +22,7 @@ import InspectedElementSuspendedBy from './InspectedElementSuspendedBy';
22 import NativeStyleEditor from './NativeStyleEditor';
23 import {enableStyleXFeatures} from 'react-devtools-feature-flags';
24 import InspectedElementSourcePanel from './InspectedElementSourcePanel';
25 +import StackTraceView from './StackTraceView';
26 import OwnerView from './OwnerView';
27
28 import styles from './InspectedElementView.css';
@@ -52,6 +53,7 @@ export default function InspectedElementView({
53 symbolicatedSourcePromise,
54 }: Props): React.Node {
55 const {
56 + stack,
57 owners,
58 rendererPackageName,
59 rendererVersion,
@@ -68,8 +70,9 @@ export default function InspectedElementView({
70 ? `${rendererPackageName}@${rendererVersion}`
71 : null;
72 const showOwnersList = owners !== null && owners.length > 0;
73 + const showStack = stack != null && stack.length > 0;
74 const showRenderedBy =
72 - showOwnersList || rendererLabel !== null || rootType !== null;
75 + showStack || showOwnersList || rendererLabel !== null || rootType !== null;
76
77 return (
78 <Fragment>
@@ -168,20 +171,26 @@ export default function InspectedElementView({
171 data-testname="InspectedElementView-Owners">
172 <div className={styles.OwnersHeader}>rendered by</div>
173
174 + {showStack ? <StackTraceView stack={stack} /> : null}
175 {showOwnersList &&
176 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 + </>
194 ))}
195
196 {rootType !== null && (
packages/react-devtools-shared/src/devtools/views/Components/OwnerView.js
+2 -1
@@ -60,7 +60,8 @@ export default function OwnerView({
60 <span className={styles.OwnerContent}>
61 <span
62 className={`${styles.Owner} ${isInStore ? '' : styles.NotInStore}`}
63 - title={displayName}>
63 + title={displayName}
64 + data-testname="OwnerView">
65 {'<' + displayName + '>'}
66 </span>
67
packages/react-devtools-shared/src/frontend/types.js
+4
@@ -216,6 +216,7 @@ export type SerializedElement = {
216 id: number,
217 key: number | string | null,
218 env: null | string,
219 + stack: null | ReactStackTrace,
220 hocDisplayNames: Array<string> | null,
221 compiledWithForget: boolean,
222 type: ElementType,
@@ -279,6 +280,9 @@ export type InspectedElement = {
280 // Location of component in source code.
281 source: ReactFunctionLocation | null,
282
283 + // The location of the JSX creation.
284 + stack: ReactStackTrace | null,
285 +
286 type: ElementType,
287
288 // Meta information about the root this element belongs to.