@samitouri / QOS-React / commits / 8d557a638e

[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) {