@samitouri / QOS-React / commits / 423c44b886

[DevTools] Don't highlight the root rect if no roots has unique suspenders (#34885)

Stacked on #34881. We don't paint suspense boundaries if there are no suspenders. This does the same with the root. The root is still selectable so you can confirm but there's no affordance drawing attention to click the root. This could happen if you don't use the built-ins of React to load things like scripts and css. It would never happen in something like Next.js where code and CSS is loaded through React-native like RSC. However, it could also happen in the Activity scoped case when all resources are always loaded early.

Sebastian Markbåge committed Oct 17, 2025 at 18:53 UTC 423c44b88611afd9bf332fd1a91b5afdca8a48be
2 files changed +33 -9
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css
+12 -7
@@ -1,22 +1,27 @@
1 .SuspenseRectsContainer {
2 padding: .25rem;
3 - cursor: pointer;
4 - outline-color: var(--color-transition);
3 + outline-color: transparent;
4 outline-style: solid;
5 outline-width: 1px;
6 border-radius: 0.25rem;
8 - background-color: color-mix(in srgb, var(--color-transition) 5%, transparent);
9 -}
10 -
11 -.SuspenseRectsContainer[data-hovered='true'] {
12 - background-color: color-mix(in srgb, var(--color-transition) 15%, transparent);
7 }
8
9 .SuspenseRectsContainer[data-highlighted='true'] {
10 + outline-color: var(--color-transition);
11 outline-style: solid;
12 outline-width: 4px;
13 }
14
15 +.SuspenseRectsRoot {
16 + cursor: pointer;
17 + outline-color: var(--color-transition);
18 + background-color: color-mix(in srgb, var(--color-transition) 5%, transparent);
19 +}
20 +
21 +.SuspenseRectsRoot[data-hovered='true'] {
22 + background-color: color-mix(in srgb, var(--color-transition) 15%, transparent);
23 +}
24 +
25 .SuspenseRectsViewBox {
26 position: relative;
27 }
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js
+21 -2
@@ -326,7 +326,9 @@ function SuspenseRectsContainer(): React$Node {
326 const treeDispatch = useContext(TreeDispatcherContext);
327 const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
328 // TODO: This relies on a full re-render of all children when the Suspense tree changes.
329 - const {roots, hoveredTimelineIndex} = useContext(SuspenseTreeStateContext);
329 + const {roots, hoveredTimelineIndex, uniqueSuspendersOnly} = useContext(
330 + SuspenseTreeStateContext,
331 + );
332
333 // TODO: bbox does not consider uniqueSuspendersOnly filter
334 const boundingBox = getDocumentBoundingRect(store, roots);
@@ -372,9 +374,26 @@ function SuspenseRectsContainer(): React$Node {
374 const isRootSelected = roots.includes(inspectedElementID);
375 const isRootHovered = hoveredTimelineIndex === 0;
376
377 + let hasRootSuspenders = false;
378 + if (!uniqueSuspendersOnly) {
379 + hasRootSuspenders = true;
380 + } else {
381 + for (let i = 0; i < roots.length; i++) {
382 + const rootID = roots[i];
383 + const root = store.getSuspenseByID(rootID);
384 + if (root !== null && root.hasUniqueSuspenders) {
385 + hasRootSuspenders = true;
386 + break;
387 + }
388 + }
389 + }
390 +
391 return (
392 <div
377 - className={styles.SuspenseRectsContainer}
393 + className={
394 + styles.SuspenseRectsContainer +
395 + (hasRootSuspenders ? ' ' + styles.SuspenseRectsRoot : '')
396 + }
397 onClick={handleClick}
398 onDoubleClick={handleDoubleClick}
399 data-highlighted={isRootSelected}