@samitouri / QOS-React / commits / be11cb5c4b

[DevTools] Tweak the presentation of the Promise value (#34097)

Show the value as "fulfilled: Type" or "rejected: Type" immediately instead of having to expand it twice. We could show all the properties of the object immediately like we do in the Performance Track but it's not always particularly interesting data in the value that isn't already in the header. I also moved it to the end after the stack traces since I think the stack is more interesting but I'm also visually trying to connect the stack trace with the "name" since typically the "name" will come from part of the stack trace. Before: <img width="517" height="433" alt="Screenshot 2025-08-03 at 11 39 49 PM" src="https://github.com/user-attachments/assets/ad28d8a2-c149-4957-a393-20ff3932a819" /> After: <img width="520" height="476" alt="Screenshot 2025-08-03 at 11 58 35 PM" src="https://github.com/user-attachments/assets/53a755b0-bb68-4305-9d16-d6fac7ca4910" />

Sebastian Markbåge committed Aug 4, 2025 at 09:42 UTC be11cb5c4b36b42dcc4c8bdcbc67d9a9b4ac2e14
4 files changed +51 -27
packages/react-client/src/ReactFlightPerformanceTrack.js
+2 -2
@@ -490,7 +490,7 @@ export function logComponentAwait(
490 if (typeof value === 'object' && value !== null) {
491 addObjectToProperties(value, properties, 0, '');
492 } else if (value !== undefined) {
493 - addValueToProperties('Resolved', value, properties, 0, '');
493 + addValueToProperties('awaited value', value, properties, 0, '');
494 }
495 const tooltipText = getIOLongName(
496 asyncInfo.awaited,
@@ -547,7 +547,7 @@ export function logIOInfoErrored(
547 String(error.message)
548 : // eslint-disable-next-line react-internal/safe-string-coercion
549 String(error);
550 - const properties = [['Rejected', message]];
550 + const properties = [['rejected with', message]];
551 const tooltipText =
552 getIOLongName(ioInfo, description, ioInfo.env, rootEnv) + ' Rejected';
553 debugTask.run(
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css
+2 -2
@@ -97,11 +97,11 @@
97 }
98
99 .CollapsableContent {
100 - padding: 0.25rem 0;
100 + margin-top: -0.25rem;
101 }
102
103 .PreviewContainer {
104 - padding: 0 0.25rem 0.25rem 0.25rem;
104 + padding: 0.25rem;
105 }
106
107 .TimeBarContainer {
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js
+36 -23
@@ -107,11 +107,10 @@ function SuspendedByRow({
107 }
108
109 const value: any = asyncInfo.awaited.value;
110 - const isErrored =
111 - value !== null &&
112 - typeof value === 'object' &&
113 - value[meta.name] === 'rejected Thenable';
114 -
110 + const metaName =
111 + value !== null && typeof value === 'object' ? value[meta.name] : null;
112 + const isFulfilled = metaName === 'fulfilled Thenable';
113 + const isRejected = metaName === 'rejected Thenable';
114 return (
115 <div className={styles.CollapsableRow}>
116 <Button
@@ -136,7 +135,7 @@ function SuspendedByRow({
135 <div className={styles.TimeBarContainer}>
136 <div
137 className={
139 - !isErrored ? styles.TimeBarSpan : styles.TimeBarSpanErrored
138 + !isRejected ? styles.TimeBarSpan : styles.TimeBarSpanErrored
139 }
140 style={{
141 left: left.toFixed(2) + '%',
@@ -147,6 +146,20 @@ function SuspendedByRow({
146 </Button>
147 {isOpen && (
148 <div className={styles.CollapsableContent}>
149 + {stack !== null && stack.length > 0 && (
150 + <StackTraceView stack={stack} />
151 + )}
152 + {owner !== null && owner.id !== inspectedElement.id ? (
153 + <OwnerView
154 + key={owner.id}
155 + displayName={owner.displayName || 'Anonymous'}
156 + hocDisplayNames={owner.hocDisplayNames}
157 + compiledWithForget={owner.compiledWithForget}
158 + id={owner.id}
159 + isInStore={store.containsElement(owner.id)}
160 + type={owner.type}
161 + />
162 + ) : null}
163 <div className={styles.PreviewContainer}>
164 <KeyValue
165 alphaSort={true}
@@ -158,27 +171,27 @@ function SuspendedByRow({
171 element={element}
172 hidden={false}
173 inspectedElement={inspectedElement}
161 - name={'Promise'}
162 - path={[index, 'awaited', 'value']}
174 + name={
175 + isFulfilled
176 + ? 'awaited value'
177 + : isRejected
178 + ? 'rejected with'
179 + : 'pending value'
180 + }
181 + path={
182 + isFulfilled
183 + ? [index, 'awaited', 'value', 'value']
184 + : isRejected
185 + ? [index, 'awaited', 'value', 'reason']
186 + : [index, 'awaited', 'value']
187 + }
188 pathRoot="suspendedBy"
189 store={store}
165 - value={asyncInfo.awaited.value}
190 + value={
191 + isFulfilled ? value.value : isRejected ? value.reason : value
192 + }
193 />
194 </div>
168 - {stack !== null && stack.length > 0 && (
169 - <StackTraceView stack={stack} />
170 - )}
171 - {owner !== null && owner.id !== inspectedElement.id ? (
172 - <OwnerView
173 - key={owner.id}
174 - displayName={owner.displayName || 'Anonymous'}
175 - hocDisplayNames={owner.hocDisplayNames}
176 - compiledWithForget={owner.compiledWithForget}
177 - id={owner.id}
178 - isInStore={store.containsElement(owner.id)}
179 - type={owner.type}
180 - />
181 - ) : null}
195 </div>
196 )}
197 </div>
packages/react-devtools-shared/src/hydration.js
+11
@@ -21,6 +21,8 @@ import type {
21 InspectedElementPath,
22 } from 'react-devtools-shared/src/frontend/types';
23
24 +import noop from 'shared/noop';
25 +
26 export const meta = {
27 inspectable: (Symbol('inspectable'): symbol),
28 inspected: (Symbol('inspected'): symbol),
@@ -317,6 +319,15 @@ export function dehydrate(
319 };
320 }
321
322 + if (
323 + data.status === 'resolved_model' ||
324 + data.status === 'resolve_module'
325 + ) {
326 + // This looks it's a lazy initialization pattern such in Flight.
327 + // Since we're about to inspect it. Let's eagerly initialize it.
328 + data.then(noop);
329 + }
330 +
331 switch (data.status) {
332 case 'fulfilled': {
333 const unserializableValue: Unserializable = {