[DevTools] Only show Suspense rects matching "unique-suspenders-only" filter (#34607)
Sebastian "Sebbie" Silbermann committed
Sep 26, 2025 at 17:29 UTC
8d557a638ef8caf9bddfc6c7541b58b8b59259f9
3 files changed
+71
-10
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css
+9
@@ -19,6 +19,10 @@
19
overflow: hidden;
20
}
21
22
+.SuspenseRectsScaledRect[data-visible='false'] > .SuspenseRectsBoundaryChildren {
23
+ overflow: initial;
24
+}
25
+
26
.SuspenseRectsRect {
27
box-shadow: var(--elevation-4);
28
pointer-events: all;
@@ -31,6 +35,11 @@
35
outline-color: var(--color-background-selected);
36
}
37
38
+.SuspenseRectsScaledRect[data-visible='false'] {
39
+ pointer-events: none;
40
+ outline-width: 0;
41
+}
42
+
43
/* highlight this boundary */
44
.SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect, .SuspenseRectsBoundary[data-highlighted='true'] > .SuspenseRectsRect {
45
background-color: var(--color-background-hover);
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js
+12
-2
@@ -34,10 +34,12 @@ import {
34
function ScaledRect({
35
className,
36
rect,
37
+ visible,
38
...props
39
}: {
40
className: string,
41
rect: Rect,
42
+ visible: boolean,
43
...
44
}): React$Node {
45
const viewBox = useContext(ViewBox);
@@ -50,6 +52,7 @@ function ScaledRect({
52
<div
53
{...props}
54
className={styles.SuspenseRectsScaledRect + ' ' + className}
55
+ data-visible={visible}
56
style={{
57
width,
58
height,
@@ -68,6 +71,7 @@ function SuspenseRects({
71
const store = useContext(StoreContext);
72
const treeDispatch = useContext(TreeDispatcherContext);
73
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
74
+ const {uniqueSuspendersOnly} = useContext(SuspenseTreeStateContext);
75
76
const {inspectedElementID} = useContext(TreeStateContext);
77
@@ -79,6 +83,7 @@ function SuspenseRects({
83
// getSuspenseByID will have already warned
84
return null;
85
}
86
+ const visible = suspense.hasUniqueSuspenders || !uniqueSuspendersOnly;
87
88
function handleClick(event: SyntheticMouseEvent) {
89
if (event.defaultPrevented) {
@@ -117,9 +122,13 @@ function SuspenseRects({
122
const boundingBox = getBoundingBox(suspense.rects);
123
124
return (
120
- <ScaledRect rect={boundingBox} className={styles.SuspenseRectsBoundary}>
125
+ <ScaledRect
126
+ rect={boundingBox}
127
+ className={styles.SuspenseRectsBoundary}
128
+ visible={visible}>
129
<ViewBox.Provider value={boundingBox}>
122
- {suspense.rects !== null &&
130
+ {visible &&
131
+ suspense.rects !== null &&
132
suspense.rects.map((rect, index) => {
133
return (
134
<ScaledRect
@@ -245,6 +254,7 @@ function SuspenseRectsContainer(): React$Node {
254
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
255
const {roots} = useContext(SuspenseTreeStateContext);
256
257
+ // TODO: bbox does not consider uniqueSuspendersOnly filter
258
const boundingBox = getDocumentBoundingRect(store, roots);
259
260
const boundingBoxWidth = boundingBox.width;
packages/react-devtools-shell/src/app/SuspenseTree/index.js
+50
-8
@@ -184,8 +184,12 @@ function EmptySuspense() {
184
// $FlowFixMe[missing-local-annot]
185
function PrimaryFallbackTest({initialSuspend}) {
186
const [suspend, setSuspend] = useState(initialSuspend);
187
- const fallbackStep = useTestSequence('fallback', Fallback1, Fallback2);
188
- const primaryStep = useTestSequence('primary', Primary1, Primary2);
187
+ const [fallbackStepIndex, fallbackStep] = useTestSequence(
188
+ 'fallback',
189
+ Fallback1,
190
+ Fallback2,
191
+ );
192
+ const [, primaryStep] = useTestSequence('primary', Primary1, Primary2);
193
return (
194
<Fragment>
195
<label>
@@ -198,7 +202,11 @@ function PrimaryFallbackTest({initialSuspend}) {
202
</label>
203
<br />
204
<Suspense fallback={fallbackStep}>
201
- {suspend ? <Never /> : primaryStep}
205
+ {suspend ? (
206
+ <Never id={`primary-fallback-test-${fallbackStepIndex}`} />
207
+ ) : (
208
+ primaryStep
209
+ )}
210
</Suspense>
211
</Fragment>
212
);
@@ -227,7 +235,7 @@ function useTestSequence(label: string, T1: any => any, T2: any => any) {
235
{next} <T2 prop={step}>goodbye</T2>
236
</Fragment>,
237
];
230
- return allSteps[step];
238
+ return [step, allSteps[step]];
239
}
240
241
function NestedSuspenseTest() {
@@ -252,7 +260,7 @@ function Parent() {
260
</Suspense>
261
<br />
262
<Suspense fallback={<Fallback1>This will never load</Fallback1>}>
255
- <Never />
263
+ <Never id="parent-never" />
264
</Suspense>
265
<br />
266
<b>
@@ -298,14 +306,48 @@ function LoadLater() {
306
Loaded! Click to suspend again.
307
</Primary1>
308
) : (
301
- <Never />
309
+ <Never id="load-later" />
310
)}
311
</Suspense>
312
);
313
}
314
307
-function Never() {
308
- throw new Promise(resolve => {});
315
+function readRecord(promise: any): any {
316
+ if (typeof React.use === 'function') {
317
+ return React.use(promise);
318
+ }
319
+ switch (promise.status) {
320
+ case 'pending':
321
+ throw promise;
322
+ case 'rejected':
323
+ throw promise.reason;
324
+ case 'fulfilled':
325
+ return promise.value;
326
+ default:
327
+ promise.status = 'pending';
328
+ promise.then(
329
+ value => {
330
+ promise.status = 'fulfilled';
331
+ promise.value = value;
332
+ },
333
+ reason => {
334
+ promise.status = 'rejected';
335
+ promise.reason = reason;
336
+ },
337
+ );
338
+ throw promise;
339
+ }
340
+}
341
+
342
+const nevers = new Map<string, Promise<empty>>();
343
+function Never({id}: {id: string}) {
344
+ let promise = nevers.get(id);
345
+ if (!promise) {
346
+ promise = new Promise(() => {});
347
+ (promise as any).displayName = id;
348
+ nevers.set(id, promise);
349
+ }
350
+ readRecord(promise);
351
}
352
353
function Fallback1({prop, ...rest}: any) {