@samitouri / QOS-React-1 / commits / 14c50e344c

[DevTools] Use Visually Lighter Skeletons (#34185)

The skeletons right now are too jarring because they're visually heavier than the content that comes in later. This makes them draw attention to themselves as flashing things. A good skeleton and loading indicator should ideally start as invisible as possible and then gradually become more visible the longer time passes so that if it loads quickly then it was never much visible at all. Even at its max it should never be heavier weight than the final content so that it visually reverts into lesser. Another rule of thumb is that it should be as close as possible to the final content in size but if it's unknown it should always be smaller than the final content so that the content grows into its slot rather than the slot contracting. This makes the skeleton fade from invisible into the dimmest color just as a subtle hint that something is still loading. I also added a missing skeleton since the stack traces in rendered by can now suspend while source mapping. The other tweak I did is use disabled buttons in all the cases where we load the ability to enable a button. This is more subtle and if you hover over you can see why it's still disabled. Rather than flashing the button each time you change element.

Sebastian Markbåge committed Aug 12, 2025 at 23:10 UTC 14c50e344c2f639a9e08cc03b16cc7fc6c1d194a
7 files changed +84 -46
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
+8 -12
@@ -24,7 +24,6 @@ import FetchFileWithCachingContext from './FetchFileWithCachingContext';
24 import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource';
25 import OpenInEditorButton from './OpenInEditorButton';
26 import InspectedElementViewSourceButton from './InspectedElementViewSourceButton';
27 -import Skeleton from './Skeleton';
27 import useEditorURL from '../useEditorURL';
28
29 import styles from './InspectedElement.css';
@@ -203,7 +202,9 @@ export default function InspectedElementWrapper(_: Props): React.Node {
202 }
203
204 return (
206 - <div className={styles.InspectedElement}>
205 + <div
206 + className={styles.InspectedElement}
207 + key={inspectedElementID /* Force reset when selected Element changes */}>
208 <div className={styles.TitleRow} data-testname="InspectedElement-Title">
209 {strictModeBadge}
210
@@ -232,13 +233,11 @@ export default function InspectedElementWrapper(_: Props): React.Node {
233 !!editorURL &&
234 source != null &&
235 symbolicatedSourcePromise != null && (
235 - <React.Suspense fallback={<Skeleton height={16} width={24} />}>
236 - <OpenInEditorButton
237 - editorURL={editorURL}
238 - source={source}
239 - symbolicatedSourcePromise={symbolicatedSourcePromise}
240 - />
241 - </React.Suspense>
236 + <OpenInEditorButton
237 + editorURL={editorURL}
238 + source={source}
239 + symbolicatedSourcePromise={symbolicatedSourcePromise}
240 + />
241 )}
242
243 {canToggleError && (
@@ -294,9 +293,6 @@ export default function InspectedElementWrapper(_: Props): React.Node {
293
294 {inspectedElement !== null && symbolicatedSourcePromise != null && (
295 <InspectedElementView
297 - key={
298 - inspectedElementID /* Force reset when selected Element changes */
299 - }
296 element={element}
297 hookNames={hookNames}
298 inspectedElement={inspectedElement}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSourcePanel.js
+6 -1
@@ -36,7 +36,12 @@ function InspectedElementSourcePanel({
36 <div className={styles.SourceHeaderRow}>
37 <div className={styles.SourceHeader}>source</div>
38
39 - <React.Suspense fallback={<Skeleton height={16} width={16} />}>
39 + <React.Suspense
40 + fallback={
41 + <Button disabled={true} title="Loading source maps...">
42 + <ButtonIcon type="copy" />
43 + </Button>
44 + }>
45 <CopySourceButton
46 source={source}
47 symbolicatedSourcePromise={symbolicatedSourcePromise}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.css
+4
@@ -24,3 +24,7 @@
24 overflow: hidden;
25 text-overflow: ellipsis;
26 }
27 +
28 +.RenderedBySkeleton {
29 + padding-left: 1.25rem;
30 +}
\ No newline at end of file
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
+35 -28
@@ -24,6 +24,7 @@ import {enableStyleXFeatures} from 'react-devtools-feature-flags';
24 import InspectedElementSourcePanel from './InspectedElementSourcePanel';
25 import StackTraceView from './StackTraceView';
26 import OwnerView from './OwnerView';
27 +import Skeleton from './Skeleton';
28
29 import styles from './InspectedElementView.css';
30
@@ -170,34 +171,40 @@ export default function InspectedElementView({
171 className={styles.InspectedElementSection}
172 data-testname="InspectedElementView-Owners">
173 <div className={styles.OwnersHeader}>rendered by</div>
173 -
174 - {showStack ? <StackTraceView stack={stack} /> : null}
175 - {showOwnersList &&
176 - owners?.map(owner => (
177 - <Fragment key={owner.id}>
178 - <OwnerView
179 - displayName={owner.displayName || 'Anonymous'}
180 - hocDisplayNames={owner.hocDisplayNames}
181 - environmentName={
182 - inspectedElement.env === owner.env ? null : owner.env
183 - }
184 - compiledWithForget={owner.compiledWithForget}
185 - id={owner.id}
186 - isInStore={store.containsElement(owner.id)}
187 - type={owner.type}
188 - />
189 - {owner.stack != null && owner.stack.length > 0 ? (
190 - <StackTraceView stack={owner.stack} />
191 - ) : null}
192 - </Fragment>
193 - ))}
194 -
195 - {rootType !== null && (
196 - <div className={styles.OwnersMetaField}>{rootType}</div>
197 - )}
198 - {rendererLabel !== null && (
199 - <div className={styles.OwnersMetaField}>{rendererLabel}</div>
200 - )}
174 + <React.Suspense
175 + fallback={
176 + <div className={styles.RenderedBySkeleton}>
177 + <Skeleton height={16} width="40%" />
178 + </div>
179 + }>
180 + {showStack ? <StackTraceView stack={stack} /> : null}
181 + {showOwnersList &&
182 + owners?.map(owner => (
183 + <Fragment key={owner.id}>
184 + <OwnerView
185 + displayName={owner.displayName || 'Anonymous'}
186 + hocDisplayNames={owner.hocDisplayNames}
187 + environmentName={
188 + inspectedElement.env === owner.env ? null : owner.env
189 + }
190 + compiledWithForget={owner.compiledWithForget}
191 + id={owner.id}
192 + isInStore={store.containsElement(owner.id)}
193 + type={owner.type}
194 + />
195 + {owner.stack != null && owner.stack.length > 0 ? (
196 + <StackTraceView stack={owner.stack} />
197 + ) : null}
198 + </Fragment>
199 + ))}
200 +
201 + {rootType !== null && (
202 + <div className={styles.OwnersMetaField}>{rootType}</div>
203 + )}
204 + {rendererLabel !== null && (
205 + <div className={styles.OwnersMetaField}>{rendererLabel}</div>
206 + )}
207 + </React.Suspense>
208 </div>
209 )}
210
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementViewSourceButton.js
+6 -2
@@ -11,7 +11,6 @@ import * as React from 'react';
11
12 import ButtonIcon from '../ButtonIcon';
13 import Button from '../Button';
14 -import Skeleton from './Skeleton';
14
15 import type {ReactFunctionLocation} from 'shared/ReactTypes';
16
@@ -27,7 +26,12 @@ function InspectedElementViewSourceButton({
26 symbolicatedSourcePromise,
27 }: Props): React.Node {
28 return (
30 - <React.Suspense fallback={<Skeleton height={16} width={24} />}>
29 + <React.Suspense
30 + fallback={
31 + <Button disabled={true} title="Loading source maps...">
32 + <ButtonIcon type="view-source" />
33 + </Button>
34 + }>
35 <ActualSourceButton
36 source={source}
37 symbolicatedSourcePromise={symbolicatedSourcePromise}
packages/react-devtools-shared/src/devtools/views/Components/Skeleton.css
+2 -2
@@ -5,9 +5,9 @@
5
6 @keyframes pulse {
7 0%, 100% {
8 - background-color: var(--color-dim);
8 + background-color: none;
9 }
10 50% {
11 - background-color: var(--color-dimmest)
11 + background-color: var(--color-dimmest);
12 }
13 }
packages/react-devtools-shared/src/devtools/views/Editor/OpenInEditorButton.js
+23 -1
@@ -24,7 +24,11 @@ type Props = {
24 className?: string,
25 };
26
27 -function OpenInEditorButton({editorURL, source, className}: Props): React.Node {
27 +function ActualOpenInEditorButton({
28 + editorURL,
29 + source,
30 + className,
31 +}: Props): React.Node {
32 let disable;
33 if (source == null) {
34 disable = true;
@@ -68,4 +72,22 @@ function OpenInEditorButton({editorURL, source, className}: Props): React.Node {
72 );
73 }
74
75 +function OpenInEditorButton({editorURL, source, className}: Props): React.Node {
76 + return (
77 + <React.Suspense
78 + fallback={
79 + <Button disabled={true} className={className}>
80 + <ButtonIcon type="editor" />
81 + <ButtonLabel>Loading source maps...</ButtonLabel>
82 + </Button>
83 + }>
84 + <ActualOpenInEditorButton
85 + editorURL={editorURL}
86 + source={source}
87 + className={className}
88 + />
89 + </React.Suspense>
90 + );
91 +}
92 +
93 export default OpenInEditorButton;