unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
refactoringtesting-strategy

[DevTools] Fix WhatChanged scrolling out of view in profiler sidebar

Project: facebook/react | Language: JavaScript | Concepts: refactoring, testing-strategy | Difficulty: mid
Link: https://github.com/react/react/pull/36244

The problem

In the React DevTools Profiler sidebar, the "WhatChanged" summary and component badges were rendered inside the same scrollable area as the commit list. When a component had many render timestamps, scrolling down the list would cause the "WhatChanged" context to scroll out of view, making it harder to analyze why a component rendered without extra manual scrolling.

Why it's non-trivial

A naive fix might just move the "WhatChanged" section outside the scrollable area, but this can create layout problems:

  • If "WhatChanged" is tall (e.g. many changed props/hooks), it could take up too much vertical space, leaving little room for the commit list.
  • The UI needs to remain accessible and usable for both mouse and keyboard users.
  • The commit list must remain scrollable independently, while the summary context should always be visible (or easily collapsible).

The approach

The PR restructures the sidebar into two regions: a sticky, optionally collapsible header for the summary/badges, and a scrollable commit list below. Key changes:

1. CSS: Split layout and add sticky header styles

.Content {
-  padding: 0.5rem;
+  padding-bottom: 0.5rem;
   user-select: none;
-  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+  height: 100%;
}

Explanation: The .Content div is now a flex column, taking full height, so children can be split into sticky and scrollable regions.

+.RenderInfo {
+  flex: 0 0 auto;
   display: flex;
   flex-direction: column;
   gap: 0.5rem;
+  padding: 0.5rem;
+  border-bottom: 1px solid var(--color-border);
+  background: var(--color-background);
+}
+
+.RenderInfoContent {
+  display: flex;
+  flex-direction: column;
+  gap: 0.75rem;
+}
+
+.CommitList {
+  flex: 1;
+  min-height: 0;
+  overflow-y: auto;
+  padding: 0.5rem;
+}

Explanation:

  • .RenderInfo is the sticky header: summary, badges, "WhatChanged".
  • .CommitList is the scrollable area for the commit list.

2. JS: Move summary and commit list into separate sibling containers

-      <div className={styles.Content} onKeyDown={handleKeyDown} tabIndex={0}>
-        {node != null && (
-          <InspectedElementBadges
-            hocDisplayNames={node.hocDisplayNames}
-            compiledWithForget={node.compiledWithForget}
-          />
-        )}
-        <WhatChanged fiberID={selectedFiberID as any as number} />
-        {listItems.length > 0 && (
-          <div>
-            <label className={styles.Label}>Rendered at: </label>
-            {listItems}
-          </div>
-        )}
-        {listItems.length === 0 && (
-          <div>Did not render on the client during this profiling session.</div>
-        )}
-      </div>
+      <div className={styles.Content}>
+        <div className={styles.RenderInfo}>
+          <div className={styles.RenderInfoContent}>
+            {node != null && (
+              <InspectedElementBadges
+                hocDisplayNames={node.hocDisplayNames}
+                compiledWithForget={node.compiledWithForget}
+              />
+            )}
+            <WhatChanged fiberID={selectedFiberID as any as number} />
+          </div>
+        </div>
+        <div className={styles.CommitList} onKeyDown={handleKeyDown}>
+          {listItems.length > 0 && (
+            <div>
+              <label className={styles.Label}>Rendered at: </label>
+              {listItems}
+            </div>
+          )}
+          {listItems.length === 0 && (
+            <div>
+              Did not render on the client during this profiling session.
+            </div>
+          )}
+        </div>
+      </div>

Explanation:

  • The summary (RenderInfo) and commit list (CommitList) are now siblings, not stacked inside the same scroll area.
  • The summary always stays visible; the commit list scrolls independently.
  • Keyboard navigation is preserved by moving onKeyDown to the commit list.

3. Badges layout updated for wrapping and spacing

-.Root {
-  user-select: none;
-  display: inline-flex;
-}
-
-.Root *:not(:first-child) {
-  margin-left: 0.25rem;
+  flex: 1 1 auto; 
+  min-width: 0;
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: start;
+  gap: 0.25rem;
 }

Explanation:

  • Badges now wrap if there are many, preventing overflow and keeping the header compact.

From the review thread

hoxyq: Also, please see failed Flow jobs, you can reproduce it locally with yarn flow dom-browser.


hoxyq: Thanks for highlighting the issue! Can we just put the WhatChanged and badges in a sticky subheader, same like we do it for the component name?

Without any expand / collapse mechanics, I think that should address the issue?

Kertsu: Yes, that was my original instinct too. Just lift WhatChanged and the badges out of the scroll container so they sit above the commit list as a static subheader.

The reason I added the collapse mechanic is for cases where WhatChanged becomes tall, e.g. when a component has many changed hooks or props, the subheader can take up a significant portion of the sidebar height, leaving little room for the commit list. The collapsible lets users trade that context off for more list space when they need it.

That said, I understand this adds interaction complexity that may not be in line with the existing DevTools patterns. Happy to drop it and go with the simpler non-collapsible sticky subheader if you think the tradeoff is acceptable.

Just want to make sure we're aligned before I push another revision.

Takeaways

  • When separating UI regions, use flex layouts to allow sticky headers and independently scrollable content.
  • Always consider the impact of large/variable-height content in sticky headers; provide a way to reclaim space if needed.
  • Keep accessibility and keyboard navigation in mind when refactoring scroll and focus management.
  • For visual context that is critical to user workflows, ensure it remains visible during navigation, not hidden by scrolling.