@samitouri / QOS-React / commits / c825f03067

[DevTools] Hide State and Props in the Sidebar for Suspense (#34630)

We're showing too much noise in the side-panel when selecting a Suspense boundary. The interesting thing to see directly is the "Suspended by". The "props" are mostly useless because the `"name"` prop is already in the tree. I'm now also showing it in the title bar of the selected element panel. The "children" and "fallback" props are just the thing that you can see in the tree view anyway. The "state" is this weird section with just one field in it, which we already have duplicated in the top toolbar as well. We can just delete this. I make sure to show the icon and a "suspended..." section while the boundary is still loading but now yet resuspended by force suspending. While still loading: <img width="600" height="193" alt="Screenshot 2025-09-27 at 11 54 37 PM" src="https://github.com/user-attachments/assets/1c3f3a96-46e0-4b11-806f-032569c7d5b5" /> After loading: <img width="602" height="266" alt="Screenshot 2025-09-27 at 11 54 53 PM" src="https://github.com/user-attachments/assets/c43cc4cb-036f-4ced-9b0d-226c6320cd76" /> Resuspended after loading: <img width="602" height="300" alt="Screenshot 2025-09-27 at 11 55 07 PM" src="https://github.com/user-attachments/assets/0be01735-48a7-47dc-b5cf-e72ec71e0148" />

Sebastian Markbåge committed Oct 2, 2025 at 15:18 UTC c825f030679c41b55faf448648cee4e41dff3787
6 files changed +33 -90
packages/react-devtools-shared/src/backend/fiber/renderer.js
+2 -5
@@ -6596,9 +6596,6 @@ export function attach(
6596 rootType = fiberRoot._debugRootType;
6597 }
6598
6599 - const isTimedOutSuspense =
6600 - tag === SuspenseComponent && memoizedState !== null;
6601 -
6599 let isErrored = false;
6600 if (isErrorBoundary(fiber)) {
6601 // if the current inspected element is an error boundary,
@@ -6670,7 +6667,7 @@ export function attach(
6667 if (
6668 fiberInstance.suspenseNode !== null &&
6669 fiberInstance.suspenseNode.hasUnknownSuspenders &&
6673 - !isTimedOutSuspense
6670 + !isSuspended
6671 ) {
6672 // Something unknown threw to suspended this boundary. Let's figure out why that might be.
6673 if (renderer.bundleType === 0) {
@@ -6708,7 +6705,7 @@ export function attach(
6705 supportsTogglingSuspense &&
6706 hasSuspenseBoundary &&
6707 // If it's showing the real content, we can always flip fallback.
6711 - (!isTimedOutSuspense ||
6708 + (!isSuspended ||
6709 // If it's showing fallback because we previously forced it to,
6710 // allow toggling it back to remove the fallback override.
6711 forceFallbackForFibers.has(fiber) ||
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
+6 -3
@@ -269,18 +269,21 @@ export default function InspectedElementWrapper(_: Props): React.Node {
269 <ButtonIcon type="error" />
270 </Toggle>
271 )}
272 - {canToggleSuspense && (
272 + {canToggleSuspense || isSuspended ? (
273 <Toggle
274 isChecked={isSuspended}
275 + isDisabled={!canToggleSuspense}
276 onChange={toggleSuspended}
277 title={
278 isSuspended
278 - ? 'Unsuspend the selected component'
279 + ? canToggleSuspense
280 + ? 'Unsuspend the selected component'
281 + : 'This boundary is still suspended'
282 : 'Suspend the selected component'
283 }>
284 <ButtonIcon type="suspend" />
285 </Toggle>
283 - )}
286 + ) : null}
287 {store.supportsInspectMatchingDOMElement && (
288 <Button
289 onClick={highlightElement}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementPropsTree.js
+14 -1
@@ -17,7 +17,10 @@ import NewKeyValue from './NewKeyValue';
17 import {alphaSortEntries, serializeDataForCopy} from '../utils';
18 import Store from '../../store';
19 import styles from './InspectedElementSharedStyles.css';
20 -import {ElementTypeClass} from 'react-devtools-shared/src/frontend/types';
20 +import {
21 + ElementTypeClass,
22 + ElementTypeSuspense,
23 +} from 'react-devtools-shared/src/frontend/types';
24 import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
25
26 import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
@@ -47,6 +50,16 @@ export default function InspectedElementPropsTree({
50 type,
51 } = inspectedElement;
52
53 + if (type === ElementTypeSuspense) {
54 + // Skip showing the props for Suspense. We want to give more real estate to the
55 + // "Suspended by" for Suspense boundaries. We could maybe show it further below
56 + // but in practice, the props of Suspense boundaries are not very useful to
57 + // inspect because the name shows in the tree already. The children in the tree
58 + // will be either the "fallback" or "children" prop which you can already inspect
59 + // but resuspending the tree.
60 + return null;
61 + }
62 +
63 const canDeletePaths =
64 type === ElementTypeClass || canEditFunctionPropsDeletePaths;
65 const canEditValues =
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js
+11
@@ -322,6 +322,17 @@ export default function InspectedElementSuspendedBy({
322 (suspendedBy == null || suspendedBy.length === 0) &&
323 inspectedElement.unknownSuspenders === UNKNOWN_SUSPENDERS_NONE
324 ) {
325 + if (inspectedElement.isSuspended) {
326 + // If we're still suspended, show a place holder until the data loads.
327 + // We don't know what we're suspended by until it has loaded.
328 + return (
329 + <div>
330 + <div className={styles.HeaderRow}>
331 + <div className={styles.Header}>suspended...</div>
332 + </div>
333 + </div>
334 + );
335 + }
336 return null;
337 }
338
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspenseToggle.js deleted
-72
@@ -1,72 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @flow
8 - */
9 -
10 -import * as React from 'react';
11 -import {OptionsContext} from '../context';
12 -import EditableValue from './EditableValue';
13 -import Store from '../../store';
14 -import {ElementTypeSuspense} from 'react-devtools-shared/src/frontend/types';
15 -import styles from './InspectedElementSharedStyles.css';
16 -
17 -import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
18 -import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
19 -
20 -type Props = {
21 - bridge: FrontendBridge,
22 - inspectedElement: InspectedElement,
23 - store: Store,
24 -};
25 -
26 -export default function InspectedElementSuspenseToggle({
27 - bridge,
28 - inspectedElement,
29 - store,
30 -}: Props): React.Node {
31 - const {readOnly} = React.useContext(OptionsContext);
32 -
33 - const {id, isSuspended, type} = inspectedElement;
34 - const canToggleSuspense = !readOnly && inspectedElement.canToggleSuspense;
35 -
36 - if (type !== ElementTypeSuspense) {
37 - return null;
38 - }
39 -
40 - const toggleSuspense = (path: any, value: boolean) => {
41 - const rendererID = store.getRendererIDForElement(id);
42 - if (rendererID !== null) {
43 - bridge.send('overrideSuspense', {
44 - id,
45 - rendererID,
46 - forceFallback: value,
47 - });
48 - }
49 - };
50 -
51 - return (
52 - <div>
53 - <div className={styles.HeaderRow}>
54 - <div className={styles.Header}>suspense</div>
55 - </div>
56 - <div className={styles.ToggleSuspenseRow}>
57 - <span className={styles.Name}>Suspended</span>
58 - {canToggleSuspense ? (
59 - // key is required to keep <EditableValue> and header row toggle button in sync
60 - <EditableValue
61 - key={isSuspended}
62 - overrideValue={toggleSuspense}
63 - path={['suspense', 'Suspended']}
64 - value={isSuspended}
65 - />
66 - ) : (
67 - <span className={styles.Value}>{isSuspended ? 'true' : 'false'}</span>
68 - )}
69 - </div>
70 - </div>
71 - );
72 -}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
-9
@@ -17,7 +17,6 @@ import InspectedElementHooksTree from './InspectedElementHooksTree';
17 import InspectedElementPropsTree from './InspectedElementPropsTree';
18 import InspectedElementStateTree from './InspectedElementStateTree';
19 import InspectedElementStyleXPlugin from './InspectedElementStyleXPlugin';
20 -import InspectedElementSuspenseToggle from './InspectedElementSuspenseToggle';
20 import InspectedElementSuspendedBy from './InspectedElementSuspendedBy';
21 import NativeStyleEditor from './NativeStyleEditor';
22 import {enableStyleXFeatures} from 'react-devtools-feature-flags';
@@ -95,14 +94,6 @@ export default function InspectedElementView({
94 />
95 </div>
96
98 - <div className={styles.InspectedElementSection}>
99 - <InspectedElementSuspenseToggle
100 - bridge={bridge}
101 - inspectedElement={inspectedElement}
102 - store={store}
103 - />
104 - </div>
105 -
97 <div className={styles.InspectedElementSection}>
98 <InspectedElementStateTree
99 bridge={bridge}