[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"
/>
This commit is contained in:
Sebastian Markbåge 2025-08-04 09:42:48 -04:00 committed by GitHub
parent 557745eb0b
commit be11cb5c4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 52 additions and 28 deletions

View File

@ -490,7 +490,7 @@ export function logComponentAwait(
if (typeof value === 'object' && value !== null) {
addObjectToProperties(value, properties, 0, '');
} else if (value !== undefined) {
addValueToProperties('Resolved', value, properties, 0, '');
addValueToProperties('awaited value', value, properties, 0, '');
}
const tooltipText = getIOLongName(
asyncInfo.awaited,
@ -547,7 +547,7 @@ export function logIOInfoErrored(
String(error.message)
: // eslint-disable-next-line react-internal/safe-string-coercion
String(error);
const properties = [['Rejected', message]];
const properties = [['rejected with', message]];
const tooltipText =
getIOLongName(ioInfo, description, ioInfo.env, rootEnv) + ' Rejected';
debugTask.run(

View File

@ -97,11 +97,11 @@
}
.CollapsableContent {
padding: 0.25rem 0;
margin-top: -0.25rem;
}
.PreviewContainer {
padding: 0 0.25rem 0.25rem 0.25rem;
padding: 0.25rem;
}
.TimeBarContainer {

View File

@ -107,11 +107,10 @@ function SuspendedByRow({
}
const value: any = asyncInfo.awaited.value;
const isErrored =
value !== null &&
typeof value === 'object' &&
value[meta.name] === 'rejected Thenable';
const metaName =
value !== null && typeof value === 'object' ? value[meta.name] : null;
const isFulfilled = metaName === 'fulfilled Thenable';
const isRejected = metaName === 'rejected Thenable';
return (
<div className={styles.CollapsableRow}>
<Button
@ -136,7 +135,7 @@ function SuspendedByRow({
<div className={styles.TimeBarContainer}>
<div
className={
!isErrored ? styles.TimeBarSpan : styles.TimeBarSpanErrored
!isRejected ? styles.TimeBarSpan : styles.TimeBarSpanErrored
}
style={{
left: left.toFixed(2) + '%',
@ -147,24 +146,6 @@ function SuspendedByRow({
</Button>
{isOpen && (
<div className={styles.CollapsableContent}>
<div className={styles.PreviewContainer}>
<KeyValue
alphaSort={true}
bridge={bridge}
canDeletePaths={false}
canEditValues={false}
canRenamePaths={false}
depth={1}
element={element}
hidden={false}
inspectedElement={inspectedElement}
name={'Promise'}
path={[index, 'awaited', 'value']}
pathRoot="suspendedBy"
store={store}
value={asyncInfo.awaited.value}
/>
</div>
{stack !== null && stack.length > 0 && (
<StackTraceView stack={stack} />
)}
@ -179,6 +160,38 @@ function SuspendedByRow({
type={owner.type}
/>
) : null}
<div className={styles.PreviewContainer}>
<KeyValue
alphaSort={true}
bridge={bridge}
canDeletePaths={false}
canEditValues={false}
canRenamePaths={false}
depth={1}
element={element}
hidden={false}
inspectedElement={inspectedElement}
name={
isFulfilled
? 'awaited value'
: isRejected
? 'rejected with'
: 'pending value'
}
path={
isFulfilled
? [index, 'awaited', 'value', 'value']
: isRejected
? [index, 'awaited', 'value', 'reason']
: [index, 'awaited', 'value']
}
pathRoot="suspendedBy"
store={store}
value={
isFulfilled ? value.value : isRejected ? value.reason : value
}
/>
</div>
</div>
)}
</div>

View File

@ -21,6 +21,8 @@ import type {
InspectedElementPath,
} from 'react-devtools-shared/src/frontend/types';
import noop from 'shared/noop';
export const meta = {
inspectable: (Symbol('inspectable'): symbol),
inspected: (Symbol('inspected'): symbol),
@ -317,6 +319,15 @@ export function dehydrate(
};
}
if (
data.status === 'resolved_model' ||
data.status === 'resolve_module'
) {
// This looks it's a lazy initialization pattern such in Flight.
// Since we're about to inspect it. Let's eagerly initialize it.
data.then(noop);
}
switch (data.status) {
case 'fulfilled': {
const unserializableValue: Unserializable = {