Repository: facebook/react
Pull request: [DevTools] Add Settings button on Suspense Tab

The settings dialog appears on all tabs and should be reachable from Suspense tab too. It's a bit weird because it's not contextual to the tab and it shows you whatever your last settings tab was opened. Maybe it should default to opening to the current tab's settings?

There aren't any Suspense specific settings yet but there definitely will be. We could move the "Show all" into settings but it might be frequently that you want to check why something isn't suspending a Suspense boundary or test SSR streaming.

However, the general settings still apply to the Suspense tab. E.g. switching dark/light mode.

<img width="857" height="233" alt="Screenshot 2025-09-27 at 12 35 05 PM" src="https://github.com/user-attachments/assets/4a38e94f-2074-4dce-906b-9a1c40bccb9b" />


Changed files:
filename: packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css
patch: @@ -14,6 +14,14 @@
   -webkit-font-smoothing: var(--font-smoothing);
 }
 
+.VRule {
+  height: 20px;
+  width: 1px;
+  flex: 0 0 1px;
+  margin: 0 0.5rem;
+  background-color: var(--color-border);
+}
+
 .TreeWrapper {
   border-top: 1px solid var(--color-border);
   flex: 1 1 65%;
@@ -107,13 +115,13 @@
 .SuspenseTreeViewHeader {
   flex: 0 0 42px;
   padding: 0.5rem;
-  display: grid;
-  grid-template-columns: auto 1fr auto auto auto;
+  display: flex;
   align-items: flex-start;
   border-bottom: 1px solid var(--color-border);
 }
 
 .SuspenseBreadcrumbs {
+  flex: 1;
   /**
    * TODO: Switch to single item view on overflow like OwnerStack does.
    * OwnerStack has more constraints that make it easier so it won't be a 1:1 port.

filename: packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js
patch: @@ -33,14 +33,17 @@ import {
   SuspenseTreeDispatcherContext,
   SuspenseTreeStateContext,
 } from './SuspenseTreeContext';
-import {StoreContext} from '../context';
+import {StoreContext, OptionsContext} from '../context';
 import {TreeDispatcherContext} from '../Components/TreeContext';
 import Button from '../Button';
 import Tooltip from '../Components/reach-ui/tooltip';
 import typeof {
   SyntheticEvent,
   SyntheticPointerEvent,
 } from 'react-dom-bindings/src/events/SyntheticEvent';
+import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
+import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
+import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
 
 type Orientation = 'horizontal' | 'vertical';
 
@@ -212,6 +215,7 @@ function ToggleInspectedElement({
 }
 
 function SuspenseTab(_: {}) {
+  const {hideSettings} = useContext(OptionsContext);
   const [state, dispatch] = useReducer<LayoutState, null, LayoutAction>(
     layoutReducer,
     null,
@@ -394,75 +398,82 @@ function SuspenseTab(_: {}) {
   };
 
   return (
-    <div className={styles.SuspenseTab} ref={wrapperTreeRef}>
-      <div className={styles.TreeWrapper} ref={resizeTreeRef}>
-        {treeListDisabled ? null : (
-          <div
-            className={styles.TreeList}
-            hidden={treeListHidden}
-            ref={resizeTreeListRef}>
-            <SuspenseTreeList />
-          </div>
-        )}
-        {treeListDisabled ? null : (
-          <div className={styles.ResizeBarWrapper} hidden={treeListHidden}>
+    <SettingsModalContextController>
+      <div className={styles.SuspenseTab} ref={wrapperTreeRef}>
+        <div className={styles.TreeWrapper} ref={resizeTreeRef}>
+          {treeListDisabled ? null : (
             <div
-              onPointerDown={onResizeStart}
-              onPointerMove={onResizeTreeList}
-              onPointerUp={onResizeEnd}
-              className={styles.ResizeBar}
-            />
-          </div>
-        )}
-        <div className={styles.TreeView}>
-          <header className={styles.SuspenseTreeViewHeader}>
-            {treeListDisabled ? (
-              <div />
-            ) : (
-              <ToggleTreeList dispatch={dispatch} state={state} />
-            )}
-            <div className={styles.SuspenseBreadcrumbs}>
-              <SuspenseBreadcrumbs />
+              className={styles.TreeList}
+              hidden={treeListHidden}
+              ref={resizeTreeListRef}>
+              <SuspenseTreeList />
             </div>
-            <ToggleUniqueSuspenders />
-            <SelectRoot />
-            <ToggleInspectedElement
-              dispatch={dispatch}
-              state={state}
-              orientation="horizontal"
-            />
-          </header>
-          <div className={styles.Rects}>
-            <SuspenseRects />
-          </div>
-          <footer className={styles.SuspenseTreeViewFooter}>
-            <div className={styles.SuspenseTimeline}>
-              <SuspenseTimeline />
+          )}
+          {treeListDisabled ? null : (
+            <div className={styles.ResizeBarWrapper} hidden={treeListHidden}>
+              <div
+                onPointerDown={onResizeStart}
+                onPointerMove={onResizeTreeList}
+                onPointerUp={onResizeEnd}
+                className={styles.ResizeBar}
+              />
+            </div>
+          )}
+          <div className={styles.TreeView}>
+            <header className={styles.SuspenseTreeViewHeader}>
+              {treeListDisabled ? (
+                <div />
+              ) : (
+                <ToggleTreeList dispatch={dispatch} state={state} />
+              )}
+              <div className={styles.SuspenseBreadcrumbs}>
+                <SuspenseBreadcrumbs />
+              </div>
+              <ToggleUniqueSuspenders />
+              <SelectRoot />
+              {!hideSettings && <div className={styles.VRule} />}
+              {!hideSettings && <SettingsModalContextToggle />}
+              <ToggleInspectedElement
+                dispatch={dispatch}
+                state={state}
+                orientation="horizontal"
+              />
+            </header>
+            <div className={styles.Rects}>
+              <SuspenseRects />
             </div>
-            <ToggleInspectedElement
-              dispatch={dispatch}
-              state={state}
-              orientation="vertical"
-            />
-          </footer>
+            <footer className={styles.SuspenseTreeViewFooter}>
+              <div className={styles.SuspenseTimeline}>
+                <SuspenseTimeline />
+              </div>
+              <ToggleInspectedElement
+                dispatch={dispatch}
+                state={state}
+                orientation="vertical"
+              />
+            </footer>
+          </div>
         </div>
-      </div>
-      <div className={styles.ResizeBarWrapper} hidden={inspectedElementHidden}>
         <div
-          onPointerDown={onResizeStart}
-          onPointerMove={onResizeTree}
-          onPointerUp={onResizeEnd}
-          className={styles.ResizeBar}
-        />
-      </div>
-      <div
-        className={styles.InspectedElementWrapper}
-        hidden={inspectedElementHidden}>
-        <InspectedElementErrorBoundary>
-          <InspectedElement />
-        </InspectedElementErrorBoundary>
+          className={styles.ResizeBarWrapper}
+          hidden={inspectedElementHidden}>
+          <div
+            onPointerDown={onResizeStart}
+            onPointerMove={onResizeTree}
+            onPointerUp={onResizeEnd}
+            className={styles.ResizeBar}
+          />
+        </div>
+        <div
+          className={styles.InspectedElementWrapper}
+          hidden={inspectedElementHidden}>
+          <InspectedElementErrorBoundary>
+            <InspectedElement />
+          </InspectedElementErrorBoundary>
+        </div>
+        <SettingsModal />
       </div>
-    </div>
+    </SettingsModalContextController>
   );
 }