From 8ab629aa4569a02e15a8bcbac8fc6e052db79cfc Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Sun, 19 Oct 2025 12:36:37 +0200 Subject: [PATCH 01/11] [DevTools] Activity slices in Suspense tab --- .../src/backend/fiber/renderer.js | 91 +++++++++- .../src/devtools/store.js | 2 +- .../src/devtools/views/Components/Element.js | 15 +- .../src/devtools/views/Components/Tree.js | 16 +- .../views/Settings/ComponentsSettings.js | 13 ++ .../views/SuspenseTab/ActivityList.css | 45 +++++ .../views/SuspenseTab/ActivityList.js | 164 ++++++++++++++++++ .../views/SuspenseTab/SuspenseTab.css | 5 +- .../devtools/views/SuspenseTab/SuspenseTab.js | 36 +++- .../views/SuspenseTab/SuspenseTreeList.js | 14 -- .../src/frontend/types.js | 13 +- .../src/app/Segments/index.js | 90 ++++++++++ .../react-devtools-shell/src/app/index.js | 2 + 13 files changed, 475 insertions(+), 31 deletions(-) create mode 100644 packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.css create mode 100644 packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js delete mode 100644 packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeList.js create mode 100644 packages/react-devtools-shell/src/app/Segments/index.js diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index ddafd014e7f..e7d6a3a6bbb 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -26,6 +26,7 @@ import { ComponentFilterHOC, ComponentFilterLocation, ComponentFilterEnvironmentName, + ComponentFilterActivitySlice, ElementTypeClass, ElementTypeContext, ElementTypeFunction, @@ -1435,16 +1436,26 @@ export function attach( const hideElementsWithPaths: Set = new Set(); const hideElementsWithTypes: Set = new Set(); const hideElementsWithEnvs: Set = new Set(); + let activitySliceID: null | FiberInstance['id'] = null; + let activitySlice: null | Fiber = null; + let isInActivitySlice: boolean = true; // Highlight updates let traceUpdatesEnabled: boolean = false; const traceUpdatesForNodes: Set = new Set(); - function applyComponentFilters(componentFilters: Array) { + function applyComponentFilters( + componentFilters: Array, + nextActivitySlice: null | Fiber = null, + ) { hideElementsWithTypes.clear(); hideElementsWithDisplayNames.clear(); hideElementsWithPaths.clear(); hideElementsWithEnvs.clear(); + // Consider everything in the slice by default + activitySliceID = null; + activitySlice = null; + isInActivitySlice = true; componentFilters.forEach(componentFilter => { if (!componentFilter.isEnabled) { @@ -1473,6 +1484,19 @@ export function attach( case ComponentFilterEnvironmentName: hideElementsWithEnvs.add(componentFilter.value); break; + case ComponentFilterActivitySlice: + if ( + nextActivitySlice !== null && + nextActivitySlice.tag === ActivityComponent + ) { + activitySlice = nextActivitySlice; + isInActivitySlice = false; + } else { + // We're not filtering by activity slice after all. + // TODO: This is not sent to the frontend. + componentFilter.isEnabled = false; + } + break; default: console.warn( `Invalid component filter type "${componentFilter.type}"`, @@ -1517,6 +1541,20 @@ export function attach( const previousForcedErrors = forceErrorForFibers.size > 0 ? new Map(forceErrorForFibers) : null; + // The ID will be based on the old tree. We need to find the Fiber based on + // that ID before we unmount everything. We set the activity slice ID once + // we mount it again. + let nextActivitySlice: null | Fiber = null; + for (let i = 0; i < componentFilters.length; i++) { + const filter = componentFilters[i]; + if (filter.type === ComponentFilterActivitySlice && filter.isEnabled) { + const instance = idToDevToolsInstanceMap.get(filter.activityID); + if (instance !== undefined && instance.kind === FIBER_INSTANCE) { + nextActivitySlice = instance.data; + } + } + } + // Recursively unmount all roots. hook.getFiberRoots(rendererID).forEach(root => { const rootInstance = rootToFiberInstanceMap.get(root); @@ -1532,7 +1570,7 @@ export function attach( currentRoot = (null: any); }); - applyComponentFilters(componentFilters); + applyComponentFilters(componentFilters, nextActivitySlice); // Reset pseudo counters so that new path selections will be persisted. rootDisplayNameCounter.clear(); @@ -1656,6 +1694,11 @@ export function attach( function shouldFilterFiber(fiber: Fiber): boolean { const {tag, type, key} = fiber; + // It is never valid to filter the root element. + if (tag !== HostRoot && !isInActivitySlice) { + return true; + } + switch (tag) { case DehydratedSuspenseComponent: // TODO: ideally we would show dehydrated Suspense immediately. @@ -4020,11 +4063,21 @@ export function attach( fiber: Fiber, traceNearestHostComponentUpdate: boolean, ): void { + const isActivitySliceEntry = + activitySlice !== null && + (fiber === activitySlice || fiber.alternate === activitySlice); + if (isActivitySliceEntry) { + isInActivitySlice = true; + } + const shouldIncludeInTree = !shouldFilterFiber(fiber); let newInstance = null; let newSuspenseNode = null; if (shouldIncludeInTree) { newInstance = recordMount(fiber, reconcilingParent); + if (isActivitySliceEntry) { + activitySliceID = newInstance.id; + } if (fiber.tag === SuspenseComponent || fiber.tag === HostRoot) { newSuspenseNode = createSuspenseNode(newInstance); // Measure this Suspense node. In general we shouldn't do this until we have @@ -4140,6 +4193,7 @@ export function attach( const stashedSuspenseParent = reconcilingParentSuspenseNode; const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode; const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes; + const stashedIsInActivitySlice = isInActivitySlice; if (newInstance !== null) { // Push a new DevTools instance parent while reconciling this subtree. reconcilingParent = newInstance; @@ -4153,6 +4207,13 @@ export function attach( remainingReconcilingChildrenSuspenseNodes = null; shouldPopSuspenseNode = true; } + if ( + !isActivitySliceEntry && + activitySlice !== null && + fiber.tag === ActivityComponent + ) { + isInActivitySlice = false; + } try { if (traceUpdatesEnabled) { if (traceNearestHostComponentUpdate) { @@ -4280,6 +4341,7 @@ export function attach( } } } finally { + isInActivitySlice = stashedIsInActivitySlice; if (newInstance !== null) { reconcilingParent = stashedParent; previouslyReconciledSibling = stashedPrevious; @@ -4311,6 +4373,7 @@ export function attach( const stashedSuspenseParent = reconcilingParentSuspenseNode; const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode; const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes; + const stashedIsInActivitySlice = isInActivitySlice; const previousSuspendedBy = instance.suspendedBy; // Push a new DevTools instance parent while reconciling this subtree. reconcilingParent = instance; @@ -4329,6 +4392,18 @@ export function attach( shouldPopSuspenseNode = true; } + if (activitySlice !== null) { + if (instance.id === activitySliceID) { + isInActivitySlice = true; + } else if ( + instance.kind === FIBER_INSTANCE && + instance.data !== null && + instance.data.tag === ActivityComponent + ) { + isInActivitySlice = false; + } + } + try { // Unmount the remaining set. if ( @@ -4379,6 +4454,7 @@ export function attach( previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; } + isInActivitySlice = stashedIsInActivitySlice; } if (instance.kind === FIBER_INSTANCE) { recordUnmount(instance); @@ -5059,6 +5135,7 @@ export function attach( const stashedSuspenseParent = reconcilingParentSuspenseNode; const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode; const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes; + const stashedIsInActivitySlice = isInActivitySlice; let updateFlags = NoUpdate; let shouldMeasureSuspenseNode = false; let shouldPopSuspenseNode = false; @@ -5098,6 +5175,15 @@ export function attach( shouldMeasureSuspenseNode = true; shouldPopSuspenseNode = true; } + + if (activitySlice !== null) { + if (fiberInstance.id === activitySliceID) { + isInActivitySlice = true; + } else if (nextFiber.tag === ActivityComponent) { + // Reached the next Activity so we're exiting the slice. + isInActivitySlice = false; + } + } } try { trackDebugInfoFromLazyType(nextFiber); @@ -5522,6 +5608,7 @@ export function attach( previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; } + isInActivitySlice = stashedIsInActivitySlice; } } } diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 5d84d12d1b8..27370d22f22 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -27,7 +27,7 @@ import { SUSPENSE_TREE_OPERATION_RESIZE, SUSPENSE_TREE_OPERATION_SUSPENDERS, } from '../constants'; -import {ElementTypeRoot} from '../frontend/types'; +import {ElementTypeActivity, ElementTypeRoot} from '../frontend/types'; import { getSavedComponentFilters, setSavedComponentFilters, diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Element.js b/packages/react-devtools-shared/src/devtools/views/Components/Element.js index 24ce695cfcf..43e611fd1bd 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Element.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Element.js @@ -8,8 +8,9 @@ */ import * as React from 'react'; -import {Fragment, useContext, useMemo, useState} from 'react'; +import {Fragment, startTransition, useContext, useMemo, useState} from 'react'; import Store from 'react-devtools-shared/src/devtools/store'; +import {ElementTypeActivity} from 'react-devtools-shared/src/frontend/types'; import ButtonIcon from '../ButtonIcon'; import {TreeDispatcherContext, TreeStateContext} from './TreeContext'; import {StoreContext} from '../context'; @@ -25,6 +26,7 @@ import styles from './Element.css'; import Icon from '../Icon'; import {useChangeOwnerAction} from './OwnersListContext'; import Tooltip from './reach-ui/tooltip'; +import {useChangeActivitySliceAction} from '../SuspenseTab/ActivityList'; type Props = { data: ItemData, @@ -65,6 +67,7 @@ export default function Element({data, index, style}: Props): React.Node { }>(errorsAndWarningsSubscription); const changeOwnerAction = useChangeOwnerAction(); + const changeActivitySliceAction = useChangeActivitySliceAction(); // Handle elements that are removed from the tree while an async render is in progress. if (element == null) { @@ -75,9 +78,13 @@ export default function Element({data, index, style}: Props): React.Node { } const handleDoubleClick = () => { - if (id !== null) { - changeOwnerAction(id); - } + startTransition(() => { + if (element.type === ElementTypeActivity) { + changeActivitySliceAction(element.id); + } else { + changeOwnerAction(element.id); + } + }); }; // $FlowFixMe[missing-local-annot] diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js index 8d763536e30..07cf8a9b1ab 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js @@ -11,6 +11,7 @@ import * as React from 'react'; import { Fragment, Suspense, + startTransition, useCallback, useContext, useEffect, @@ -37,7 +38,9 @@ import ButtonIcon from '../ButtonIcon'; import Button from '../Button'; import {logEvent} from 'react-devtools-shared/src/Logger'; import {useExtensionComponentsPanelVisibility} from 'react-devtools-shared/src/frontend/hooks/useExtensionComponentsPanelVisibility'; +import {ElementTypeActivity} from 'react-devtools-shared/src/frontend/types'; import {useChangeOwnerAction} from './OwnersListContext'; +import {useChangeActivitySliceAction} from '../SuspenseTab/ActivityList'; // Indent for each node at level N, compared to node at level N - 1. const INDENTATION_SIZE = 10; @@ -302,6 +305,7 @@ export default function Tree(): React.Node { const handleBlur = useCallback(() => setTreeFocused(false), []); const handleFocus = useCallback(() => setTreeFocused(true), []); + const changeActivitySliceAction = useChangeActivitySliceAction(); const changeOwnerAction = useChangeOwnerAction(); const handleKeyPress = useCallback( (event: $FlowFixMe) => { @@ -309,7 +313,17 @@ export default function Tree(): React.Node { case 'Enter': case ' ': if (inspectedElementID !== null) { - changeOwnerAction(inspectedElementID); + const inspectedElement = store.getElementByID(inspectedElementID); + startTransition(() => { + if ( + inspectedElement !== null && + inspectedElement.type === ElementTypeActivity + ) { + changeActivitySliceAction(inspectedElementID); + } else { + changeOwnerAction(inspectedElementID); + } + }); } break; default: diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js index 33e98835bb1..12de007eaac 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js @@ -29,6 +29,7 @@ import { ComponentFilterHOC, ComponentFilterLocation, ComponentFilterEnvironmentName, + ComponentFilterActivitySlice, ElementTypeClass, ElementTypeContext, ElementTypeFunction, @@ -171,6 +172,8 @@ export default function ComponentsSettings({ isValid: true, value: 'Client', }; + } else if (type === ComponentFilterActivitySlice) { + // TODO: Allow changing type } } return cloned; @@ -371,6 +374,9 @@ export default function ComponentsSettings({ : styles.InvalidRegExp } isChecked={componentFilter.isEnabled} + isDisabled={ + componentFilter.type === ComponentFilterActivitySlice + } onChange={isEnabled => toggleFilterIsEnabled(componentFilter, isEnabled) } @@ -392,6 +398,9 @@ export default function ComponentsSettings({ @@ -487,6 +497,9 @@ export default function ComponentsSettings({ ))} )} + {componentFilter.type === ComponentFilterActivitySlice && ( + Activity Slice + )} + +
+ +
+ ); +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js index 07cf8a9b1ab..5623d507a3b 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js @@ -41,6 +41,7 @@ import {useExtensionComponentsPanelVisibility} from 'react-devtools-shared/src/f import {ElementTypeActivity} from 'react-devtools-shared/src/frontend/types'; import {useChangeOwnerAction} from './OwnersListContext'; import {useChangeActivitySliceAction} from '../SuspenseTab/ActivityList'; +import ActivitySlice from './ActivitySlice'; // Indent for each node at level N, compared to node at level N - 1. const INDENTATION_SIZE = 10; @@ -75,6 +76,7 @@ function calculateInitialScrollOffset( export default function Tree(): React.Node { const dispatch = useContext(TreeDispatcherContext); const { + activityID, numElements, ownerID, searchIndex, @@ -458,7 +460,13 @@ export default function Tree(): React.Node { )} }> - {ownerID !== null ? : } + {ownerID !== null ? ( + + ) : activityID !== null ? ( + + ) : ( + + )} {ownerID === null && (errors > 0 || warnings > 0) && ( diff --git a/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js b/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js index 72556543f4b..4ad28c38935 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js @@ -57,6 +57,9 @@ export type StateContext = { ownerID: number | null, ownerFlatTree: Array | null, + // Activity slice + activityID: Element['id'] | null, + // Inspection element panel inspectedElementID: number | null, inspectedElementIndex: number | null, @@ -70,7 +73,7 @@ type ACTION_GO_TO_PREVIOUS_SEARCH_RESULT = { }; type ACTION_HANDLE_STORE_MUTATION = { type: 'HANDLE_STORE_MUTATION', - payload: [Array, Map], + payload: [Array, Map, null | Element['id']], }; type ACTION_RESET_OWNER_STACK = { type: 'RESET_OWNER_STACK', @@ -167,6 +170,9 @@ type State = { ownerID: number | null, ownerFlatTree: Array | null, + // Activity slice + activityID: Element['id'] | null, + // Inspection element panel inspectedElementID: number | null, inspectedElementIndex: number | null, @@ -794,6 +800,33 @@ function reduceOwnersState(store: Store, state: State, action: Action): State { }; } +function reduceActivityState( + store: Store, + state: State, + action: Action, +): State { + switch (action.type) { + case 'HANDLE_STORE_MUTATION': + let {activityID} = state; + const [, , activitySliceIDChange] = action.payload; + if (activitySliceIDChange === 0 && activityID !== null) { + activityID = null; + } else if ( + activitySliceIDChange !== null && + activitySliceIDChange !== activityID + ) { + activityID = activitySliceIDChange; + } + if (activityID !== state.activityID) { + return { + ...state, + activityID, + }; + } + } + return state; +} + type Props = { children: React$Node, @@ -828,6 +861,9 @@ function getInitialState({ ownerID: defaultOwnerID == null ? null : defaultOwnerID, ownerFlatTree: null, + // Activity slice + activityID: null, + // Inspection element panel inspectedElementID: defaultInspectedElementID != null @@ -882,6 +918,7 @@ function TreeContextController({ state = reduceTreeState(store, state, action); state = reduceSearchState(store, state, action); state = reduceOwnersState(store, state, action); + state = reduceActivityState(store, state, action); // TODO(hoxyq): review // If the selected ID is in a collapsed subtree, reset the selected index to null. @@ -950,13 +987,14 @@ function TreeContextController({ // Mutations to the underlying tree may impact this context (e.g. search results, selection state). useEffect(() => { - const handleStoreMutated = ([addedElementIDs, removedElementIDs]: [ - Array, - Map, - ]) => { + const handleStoreMutated = ([ + addedElementIDs, + removedElementIDs, + activitySliceIDChange, + ]: [Array, Map, null | Element['id']]) => { dispatch({ type: 'HANDLE_STORE_MUTATION', - payload: [addedElementIDs, removedElementIDs], + payload: [addedElementIDs, removedElementIDs, activitySliceIDChange], }); }; @@ -967,7 +1005,7 @@ function TreeContextController({ // It would only impact the search state, which is unlikely to exist yet at this point. dispatch({ type: 'HANDLE_STORE_MUTATION', - payload: [[], new Map()], + payload: [[], new Map(), null], }); } diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js index d2226a183a1..41c5c7a2c09 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js @@ -16,6 +16,7 @@ import { TREE_OPERATION_SET_SUBTREE_MODE, TREE_OPERATION_UPDATE_TREE_BASE_DURATION, TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS, + TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE, SUSPENSE_TREE_OPERATION_ADD, SUSPENSE_TREE_OPERATION_REMOVE, SUSPENSE_TREE_OPERATION_REORDER_CHILDREN, @@ -475,6 +476,20 @@ function updateTree( break; } + case TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE: { + i++; + const activitySliceIDChange = operations[i++]; + if (__DEBUG__) { + debug( + 'Applied activity slice change', + activitySliceIDChange === 0 + ? 'Reset applied activity slice' + : `Changed to activity slice ID ${activitySliceIDChange}`, + ); + } + break; + } + default: throw Error(`Unsupported Bridge operation "${operation}"`); } diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js index 03862162525..b5f706abd50 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js @@ -27,10 +27,12 @@ import { import {useHighlightHostInstance} from '../hooks'; import {StoreContext} from '../context'; -export function useChangeActivitySliceAction(): (id: Element['id']) => void { +export function useChangeActivitySliceAction(): ( + id: Element['id'] | null, +) => void { const store = useContext(StoreContext); - function changeActivitySliceAction(activityID: Element['id']) { + function changeActivitySliceAction(activityID: Element['id'] | null) { const nextFilters: ComponentFilter[] = []; // Remove any existing activity slice filter for (let i = 0; i < store.componentFilters.length; i++) { @@ -39,14 +41,16 @@ export function useChangeActivitySliceAction(): (id: Element['id']) => void { nextFilters.push(filter); } } - const activityFilter: ActivitySliceFilter = { - type: 6, - activityID: activityID, - isValid: true, - isEnabled: true, - }; - nextFilters.push(activityFilter); + if (activityID !== null) { + const activityFilter: ActivitySliceFilter = { + type: 6, + activityID: activityID, + isValid: true, + isEnabled: true, + }; + nextFilters.push(activityFilter); + } store.componentFilters = nextFilters; } diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index 943c7e8bc93..c84ed2d3647 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -33,6 +33,7 @@ import { TREE_OPERATION_SET_SUBTREE_MODE, TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS, TREE_OPERATION_UPDATE_TREE_BASE_DURATION, + TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE, LOCAL_STORAGE_COMPONENT_FILTER_PREFERENCES_KEY, LOCAL_STORAGE_OPEN_IN_EDITOR_URL, LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET, @@ -444,6 +445,16 @@ export function printOperationsArray(operations: Array) { break; } + case TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE: { + i++; + const activitySliceIDChange = operations[i + 1]; + logs.push( + activitySliceIDChange === 0 + ? 'Reset applied activity slice' + : 'Applied activity slice change to ' + activitySliceIDChange, + ); + break; + } default: throw Error(`Unsupported Bridge operation "${operation}"`); } From 0e10c613dd4e8726a876bfe2f975eb44a3fd1a05 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Mon, 20 Oct 2025 02:06:00 +0200 Subject: [PATCH 06/11] Polish settings --- .../views/Settings/ComponentsSettings.js | 61 ++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js index 12de007eaac..9b73e949429 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js @@ -367,34 +367,33 @@ export default function ComponentsSettings({ {componentFilters.map((componentFilter, index) => ( - - toggleFilterIsEnabled(componentFilter, isEnabled) - } - title={ - componentFilter.isValid === false - ? 'Filter invalid' - : componentFilter.isEnabled - ? 'Filter enabled' - : 'Filter disabled' - }> - - + isChecked={componentFilter.isEnabled} + onChange={isEnabled => + toggleFilterIsEnabled(componentFilter, isEnabled) + } + title={ + componentFilter.isValid === false + ? 'Filter invalid' + : componentFilter.isEnabled + ? 'Filter enabled' + : 'Filter disabled' + }> + + + )} @@ -432,6 +435,8 @@ export default function ComponentsSettings({ {(componentFilter.type === ComponentFilterLocation || componentFilter.type === ComponentFilterDisplayName) && 'matches'} + {componentFilter.type === ComponentFilterActivitySlice && + 'within'} {componentFilter.type === ComponentFilterElementType && ( From 83fa375077a36bf67139b701424678532787eb2d Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Mon, 20 Oct 2025 23:00:23 +0200 Subject: [PATCH 07/11] Don't mutate while diffing --- .../__tests__/storeComponentFilters-test.js | 21 ++++++++++++------- .../src/backend/fiber/renderer.js | 4 ++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js index 106a39de8cd..2766f8f8adc 100644 --- a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js @@ -910,14 +910,21 @@ describe('Store component filters', () => { expect(store).toMatchInlineSnapshot(` [root] - ▾ - ▾ -

- ▾
- ▾ - + ▾ + ▾ + ▾ +

+ ▾
+ ▾ + ▾ +

+ ▾
+ ▾ + ▾ +
[suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:13,height:1}]} - + + `); }); }); diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 0d3486bf762..3c25bebd6da 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -1494,8 +1494,8 @@ export function attach( isInActivitySlice = false; } else { // We're not filtering by activity slice after all. - // TODO: This is not sent to the frontend. - componentFilter.isEnabled = false; + // Don't mark the filter as disabled here. + // Otherwise updateComponentFilters() will think no enabled filter was changed. } break; default: From c35c41a53cd3e7541714cf5058de0cedc873a27d Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Fri, 7 Nov 2025 14:21:11 +0100 Subject: [PATCH 08/11] Backend for cutting off top, frontend for cutting off bottom --- .../__tests__/storeComponentFilters-test.js | 3 +- .../src/backend/fiber/renderer.js | 83 ++++++++++--------- .../src/devtools/store.js | 57 ++++++++++++- .../src/app/Segments/index.js | 32 ++++--- 4 files changed, 121 insertions(+), 54 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js index 2766f8f8adc..45b89ecfe3b 100644 --- a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js @@ -901,9 +901,10 @@ describe('Store component filters', () => {

- + ▸ [suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:13,height:1}]} + `); await actAsync(async () => (store.componentFilters = [])); diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 3c25bebd6da..6cd998b5b29 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -1437,9 +1437,9 @@ export function attach( const hideElementsWithPaths: Set = new Set(); const hideElementsWithTypes: Set = new Set(); const hideElementsWithEnvs: Set = new Set(); - let activitySliceID: null | FiberInstance['id'] = null; - let activitySlice: null | Fiber = null; - let isInActivitySlice: boolean = true; + let focusedActivityID: null | FiberInstance['id'] = null; + let focusedActivity: null | Fiber = null; + let isInFocusedActivity: boolean = true; // Highlight updates let traceUpdatesEnabled: boolean = false; @@ -1454,9 +1454,9 @@ export function attach( hideElementsWithPaths.clear(); hideElementsWithEnvs.clear(); // Consider everything in the slice by default - activitySliceID = null; - activitySlice = null; - isInActivitySlice = true; + focusedActivityID = null; + focusedActivity = null; + isInFocusedActivity = true; componentFilters.forEach(componentFilter => { if (!componentFilter.isEnabled) { @@ -1490,8 +1490,8 @@ export function attach( nextActivitySlice !== null && nextActivitySlice.tag === ActivityComponent ) { - activitySlice = nextActivitySlice; - isInActivitySlice = false; + focusedActivity = nextActivitySlice; + isInFocusedActivity = false; } else { // We're not filtering by activity slice after all. // Don't mark the filter as disabled here. @@ -1543,13 +1543,13 @@ export function attach( // The ID will be based on the old tree. We need to find the Fiber based on // that ID before we unmount everything. We set the activity slice ID once // we mount it again. - let nextActivitySlice: null | Fiber = null; + let nextFocusedActivity: null | Fiber = null; for (let i = 0; i < componentFilters.length; i++) { const filter = componentFilters[i]; if (filter.type === ComponentFilterActivitySlice && filter.isEnabled) { const instance = idToDevToolsInstanceMap.get(filter.activityID); if (instance !== undefined && instance.kind === FIBER_INSTANCE) { - nextActivitySlice = instance.data; + nextFocusedActivity = instance.data; } } } @@ -1569,13 +1569,13 @@ export function attach( currentRoot = (null: any); }); - if (nextActivitySlice !== activitySlice) { - // Set the applied slice to 0 for now. + if (nextFocusedActivity !== focusedActivity) { // When we find the applied instance during mount we will send the actual ID. + // Otherwise 0 will indicate that we unfocused the activity slice. pushOperation(TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE); pushOperation(0); } - applyComponentFilters(componentFilters, nextActivitySlice); + applyComponentFilters(componentFilters, nextFocusedActivity); // Reset pseudo counters so that new path selections will be persisted. rootDisplayNameCounter.clear(); @@ -1700,7 +1700,7 @@ export function attach( const {tag, type, key} = fiber; // It is never valid to filter the root element. - if (tag !== HostRoot && !isInActivitySlice) { + if (tag !== HostRoot && !isInFocusedActivity) { return true; } @@ -4068,11 +4068,11 @@ export function attach( fiber: Fiber, traceNearestHostComponentUpdate: boolean, ): void { - const isActivitySliceEntry = - activitySlice !== null && - (fiber === activitySlice || fiber.alternate === activitySlice); - if (isActivitySliceEntry) { - isInActivitySlice = true; + const isFocusedActivityEntry = + focusedActivity !== null && + (fiber === focusedActivity || fiber.alternate === focusedActivity); + if (isFocusedActivityEntry) { + isInFocusedActivity = true; } const shouldIncludeInTree = !shouldFilterFiber(fiber); @@ -4080,8 +4080,8 @@ export function attach( let newSuspenseNode = null; if (shouldIncludeInTree) { newInstance = recordMount(fiber, reconcilingParent); - if (isActivitySliceEntry) { - activitySliceID = newInstance.id; + if (isFocusedActivityEntry) { + focusedActivityID = newInstance.id; pushOperation(TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE); pushOperation(newInstance.id); } @@ -4200,7 +4200,7 @@ export function attach( const stashedSuspenseParent = reconcilingParentSuspenseNode; const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode; const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes; - const stashedIsInActivitySlice = isInActivitySlice; + const stashedIsInActivitySlice = isInFocusedActivity; if (newInstance !== null) { // Push a new DevTools instance parent while reconciling this subtree. reconcilingParent = newInstance; @@ -4215,11 +4215,15 @@ export function attach( shouldPopSuspenseNode = true; } if ( - !isActivitySliceEntry && - activitySlice !== null && + !isFocusedActivityEntry && + focusedActivity !== null && fiber.tag === ActivityComponent ) { - isInActivitySlice = false; + // We're not filtering how Activity within the focused activity. + // We cut of the bottom in the Frontend if we want to just show the + // Activity slice instead of all Activity descendants. + // The filtering in the backend only happens because filtering out + // everything above the focused Activity is hard to implement in the frontend. } try { if (traceUpdatesEnabled) { @@ -4348,7 +4352,7 @@ export function attach( } } } finally { - isInActivitySlice = stashedIsInActivitySlice; + isInFocusedActivity = stashedIsInActivitySlice; if (newInstance !== null) { reconcilingParent = stashedParent; previouslyReconciledSibling = stashedPrevious; @@ -4380,7 +4384,7 @@ export function attach( const stashedSuspenseParent = reconcilingParentSuspenseNode; const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode; const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes; - const stashedIsInActivitySlice = isInActivitySlice; + const stashedIsInActivitySlice = isInFocusedActivity; const previousSuspendedBy = instance.suspendedBy; // Push a new DevTools instance parent while reconciling this subtree. reconcilingParent = instance; @@ -4399,15 +4403,16 @@ export function attach( shouldPopSuspenseNode = true; } - if (activitySlice !== null) { - if (instance.id === activitySliceID) { - isInActivitySlice = true; + if (focusedActivity !== null) { + if (instance.id === focusedActivityID) { + isInFocusedActivity = true; } else if ( instance.kind === FIBER_INSTANCE && instance.data !== null && instance.data.tag === ActivityComponent ) { - isInActivitySlice = false; + // Filtering nested Activity components inside the focused activity + // is done in the frontend. } } @@ -4461,7 +4466,7 @@ export function attach( previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; } - isInActivitySlice = stashedIsInActivitySlice; + isInFocusedActivity = stashedIsInActivitySlice; } if (instance.kind === FIBER_INSTANCE) { recordUnmount(instance); @@ -5142,7 +5147,7 @@ export function attach( const stashedSuspenseParent = reconcilingParentSuspenseNode; const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode; const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes; - const stashedIsInActivitySlice = isInActivitySlice; + const stashedIsInActivitySlice = isInFocusedActivity; let updateFlags = NoUpdate; let shouldMeasureSuspenseNode = false; let shouldPopSuspenseNode = false; @@ -5183,12 +5188,12 @@ export function attach( shouldPopSuspenseNode = true; } - if (activitySlice !== null) { - if (fiberInstance.id === activitySliceID) { - isInActivitySlice = true; + if (focusedActivity !== null) { + if (fiberInstance.id === focusedActivityID) { + isInFocusedActivity = true; } else if (nextFiber.tag === ActivityComponent) { - // Reached the next Activity so we're exiting the slice. - isInActivitySlice = false; + // Filtering nested Activity components inside the focused activity + // is done in the frontend. } } } @@ -5615,7 +5620,7 @@ export function attach( previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; } - isInActivitySlice = stashedIsInActivitySlice; + isInFocusedActivity = stashedIsInActivitySlice; } } } diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 574e5cd1ed9..dfe9184072b 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -28,7 +28,7 @@ import { SUSPENSE_TREE_OPERATION_RESIZE, SUSPENSE_TREE_OPERATION_SUSPENDERS, } from '../constants'; -import {ElementTypeRoot} from '../frontend/types'; +import {ElementTypeRoot, ElementTypeActivity} from '../frontend/types'; import { getSavedComponentFilters, setSavedComponentFilters, @@ -2073,6 +2073,32 @@ export default class Store extends EventEmitter<{ console.groupEnd(); } + if (nextActivitySliceID !== null && nextActivitySliceID !== 0) { + let didCollapse = false; + // The backend filtered everything above the Activity slice. + // We need to hide everything below the Activity slice by collapsing + // the Activities that are descendants of the next Activity slice. + const nextActivitySlice = this._idToElement.get(nextActivitySliceID); + if (nextActivitySlice === undefined) { + throw new Error('Next Activity slice not found in Store.'); + } + + for (let j = 0; j < nextActivitySlice.children.length; j++) { + didCollapse ||= this._collapseActivitiesRecursively( + nextActivitySlice.children[j], + ); + } + + if (didCollapse) { + let weightAcrossRoots = 0; + this._roots.forEach(rootID => { + const {weight} = ((this.getElementByID(rootID): any): Element); + weightAcrossRoots += weight; + }); + this._weightAcrossRoots = weightAcrossRoots; + } + } + this.emit('mutated', [ addedElementIDs, removedElementIDs, @@ -2080,6 +2106,35 @@ export default class Store extends EventEmitter<{ ]); }; + _collapseActivitiesRecursively(elementID: number): boolean { + let didMutate = false; + const element = this._idToElement.get(elementID); + if (element === undefined) { + throw new Error('Element not found in Store.'); + } + + if (element.type === ElementTypeActivity) { + if (!element.isCollapsed) { + element.isCollapsed = true; + + const weightDelta = 1 - element.weight; + + let parentElement = this._idToElement.get(element.parentID); + while (parentElement !== undefined) { + parentElement.weight += weightDelta; + parentElement = this._idToElement.get(parentElement.parentID); + } + return true; + } + return false; + } + + for (let i = 0; i < element.children.length; i++) { + didMutate ||= this._collapseActivitiesRecursively(element.children[i]); + } + return didMutate; + } + // Certain backends save filters on a per-domain basis. // In order to prevent filter preferences and applied filters from being out of sync, // this message enables the backend to override the frontend's current ("saved") filters. diff --git a/packages/react-devtools-shell/src/app/Segments/index.js b/packages/react-devtools-shell/src/app/Segments/index.js index 10d19b04b39..057ba71d1c7 100644 --- a/packages/react-devtools-shell/src/app/Segments/index.js +++ b/packages/react-devtools-shell/src/app/Segments/index.js @@ -39,33 +39,39 @@ function Page(): React.Node { function InnerSegment({children}: {children: React.Node}): React.Node { return ( - Loading...

}> + <>

Inner Segment

-
{children}
-

After inner

-
+ Loading...

}> +
{children}
+

After inner

+
+ ); } const cookies = deferred(200, 'Cookies: 🍪🍪🍪', 'cookies'); function OuterSegment({children}: {children: React.Node}): React.Node { return ( - Loading outer

}> + <>

Outer Segment

-

{cookies}

-
{children}
-

After outer

-
+ Loading outer

}> +

{cookies}

+
{children}
+

After outer

+
+ ); } function Root({children}: {children: React.Node}): React.Node { return ( - Loading root

}> + <>

Root Segment

-
{children}
-
After root
-
+ Loading root

}> +
{children}
+
After root
+
+ ); } From 9b496afdf0e91cad52e3eff7b8e4ce5d2eef7751 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Fri, 7 Nov 2025 17:26:57 +0100 Subject: [PATCH 09/11] Filter virtual instances above the slice --- packages/react-devtools-shared/src/backend/fiber/renderer.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 6cd998b5b29..761fffe01de 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -1664,6 +1664,10 @@ export function attach( data: ReactComponentInfo, secondaryEnv: null | string, ): boolean { + if (!isInFocusedActivity) { + return true; + } + // For purposes of filtering Server Components are always Function Components. // Environment will be used to filter Server vs Client. // Technically they can be forwardRef and memo too but those filters will go away From ad549585f85408b1348ff4e043a8e2cb3d0ad4e9 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Sat, 8 Nov 2025 17:11:12 +0100 Subject: [PATCH 10/11] Filter across renderers --- .../src/backend/fiber/renderer.js | 36 +++++++++++++++---- .../views/SuspenseTab/ActivityList.js | 9 +++-- .../src/frontend/types.js | 1 + 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 761fffe01de..2cd037f78d4 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -172,6 +172,7 @@ import type { } from '../types'; import type { ComponentFilter, + ActivitySliceFilter, ElementType, Plugins, } from 'react-devtools-shared/src/frontend/types'; @@ -870,6 +871,9 @@ const idToDevToolsInstanceMap: Map< FiberInstance | VirtualInstance, > = new Map(); +let focusedActivityID: null | FiberInstance['id'] = null; +let focusedActivity: null | Fiber = null; + const idToSuspenseNodeMap: Map = new Map(); // Map of canonical HostInstances to the nearest parent DevToolsInstance. @@ -1437,8 +1441,6 @@ export function attach( const hideElementsWithPaths: Set = new Set(); const hideElementsWithTypes: Set = new Set(); const hideElementsWithEnvs: Set = new Set(); - let focusedActivityID: null | FiberInstance['id'] = null; - let focusedActivity: null | Fiber = null; let isInFocusedActivity: boolean = true; // Highlight updates @@ -1447,15 +1449,16 @@ export function attach( function applyComponentFilters( componentFilters: Array, - nextActivitySlice: null | Fiber = null, + nextActivitySlice: null | Fiber, ) { hideElementsWithTypes.clear(); hideElementsWithDisplayNames.clear(); hideElementsWithPaths.clear(); hideElementsWithEnvs.clear(); - // Consider everything in the slice by default + const previousFocusedActivityID = focusedActivityID; focusedActivityID = null; focusedActivity = null; + // Consider everything in the slice by default isInFocusedActivity = true; componentFilters.forEach(componentFilter => { @@ -1492,6 +1495,12 @@ export function attach( ) { focusedActivity = nextActivitySlice; isInFocusedActivity = false; + if (componentFilter.rendererID !== rendererID) { + // We filtered an Activity from another renderer. + // We need to restore the instance ID since we won't be mounting it + // in this renderer. + focusedActivityID = previousFocusedActivityID; + } } else { // We're not filtering by activity slice after all. // Don't mark the filter as disabled here. @@ -1513,7 +1522,7 @@ export function attach( if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ != null) { const restoredComponentFilters: Array = persistableComponentFilters(window.__REACT_DEVTOOLS_COMPONENT_FILTERS__); - applyComponentFilters(restoredComponentFilters); + applyComponentFilters(restoredComponentFilters, null); } else { // Unfortunately this feature is not expected to work for React Native for now. // It would be annoying for us to spam YellowBox warnings with unactionable stuff, @@ -1521,7 +1530,7 @@ export function attach( //console.warn('⚛ DevTools: Could not locate saved component filters'); // Fallback to assuming the default filters in this case. - applyComponentFilters(getDefaultComponentFilters()); + applyComponentFilters(getDefaultComponentFilters(), null); } // If necessary, we can revisit optimizing this operation. @@ -1544,9 +1553,11 @@ export function attach( // that ID before we unmount everything. We set the activity slice ID once // we mount it again. let nextFocusedActivity: null | Fiber = null; + let focusedActivityFilter: null | ActivitySliceFilter = null; for (let i = 0; i < componentFilters.length; i++) { const filter = componentFilters[i]; if (filter.type === ComponentFilterActivitySlice && filter.isEnabled) { + focusedActivityFilter = filter; const instance = idToDevToolsInstanceMap.get(filter.activityID); if (instance !== undefined && instance.kind === FIBER_INSTANCE) { nextFocusedActivity = instance.data; @@ -1569,7 +1580,11 @@ export function attach( currentRoot = (null: any); }); - if (nextFocusedActivity !== focusedActivity) { + if ( + nextFocusedActivity !== focusedActivity && + (focusedActivityFilter === null || + focusedActivityFilter.rendererID === rendererID) + ) { // When we find the applied instance during mount we will send the actual ID. // Otherwise 0 will indicate that we unfocused the activity slice. pushOperation(TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE); @@ -1635,6 +1650,13 @@ export function attach( currentRoot = (null: any); }); + // We need to write back the new ID for the focused Fiber. + // Otherwise subsequent filter applications will try to focus based on the old ID. + // This is also relevant to filter across renderers. + if (focusedActivityFilter !== null && focusedActivityID !== null) { + focusedActivityFilter.activityID = focusedActivityID; + } + flushPendingEvents(); needsToFlushComponentLogs = false; diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js index b5f706abd50..97218fbbe4c 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js @@ -43,9 +43,14 @@ export function useChangeActivitySliceAction(): ( } if (activityID !== null) { + const rendererID = store.getRendererIDForElement(activityID); + if (rendererID === null) { + throw new Error('Expected to find renderer.'); + } const activityFilter: ActivitySliceFilter = { - type: 6, - activityID: activityID, + type: ComponentFilterActivitySlice, + activityID, + rendererID, isValid: true, isEnabled: true, }; diff --git a/packages/react-devtools-shared/src/frontend/types.js b/packages/react-devtools-shared/src/frontend/types.js index 69c5af103e1..8719d46e219 100644 --- a/packages/react-devtools-shared/src/frontend/types.js +++ b/packages/react-devtools-shared/src/frontend/types.js @@ -119,6 +119,7 @@ export type EnvironmentNameComponentFilter = { export type ActivitySliceFilter = { type: 6, activityID: Element['id'], + rendererID: number, isValid: boolean, isEnabled: boolean, }; From e86dd12456237e7a00c8858fe20243abfedc0b19 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Sat, 8 Nov 2025 17:30:21 +0100 Subject: [PATCH 11/11] Allow updating unrelated filters while focusing a Fiber --- .../src/devtools/store.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index dfe9184072b..420817198b7 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -28,7 +28,11 @@ import { SUSPENSE_TREE_OPERATION_RESIZE, SUSPENSE_TREE_OPERATION_SUSPENDERS, } from '../constants'; -import {ElementTypeRoot, ElementTypeActivity} from '../frontend/types'; +import { + ElementTypeRoot, + ElementTypeActivity, + ComponentFilterActivitySlice, +} from '../frontend/types'; import { getSavedComponentFilters, setSavedComponentFilters, @@ -2099,6 +2103,18 @@ export default class Store extends EventEmitter<{ } } + for (let j = 0; j < this._componentFilters.length; j++) { + const filter = this._componentFilters[j]; + // If we're focusing an Activity, IDs may have changed. + if (filter.type === ComponentFilterActivitySlice) { + if (nextActivitySliceID === null || nextActivitySliceID === 0) { + filter.isValid = false; + } else { + filter.activityID = nextActivitySliceID; + } + } + } + this.emit('mutated', [ addedElementIDs, removedElementIDs,