@samitouri / QOS-React-1 / commits / c9ddee7e36

[DevTools] Reset forced states when changing component filters (#34929)

Sebastian "Sebbie" Silbermann committed Oct 31, 2025 at 12:57 UTC c9ddee7e36c64311ad4e5e7acfca6c092d8a6264
3 files changed +128
packages/react-devtools-shared/src/__tests__/setupTests.js
+10
@@ -116,6 +116,16 @@ function shouldIgnoreConsoleErrorOrWarn(args) {
116 return false;
117 }
118
119 + const maybeError = args[1];
120 + if (
121 + maybeError !== null &&
122 + typeof maybeError === 'object' &&
123 + maybeError.message === 'Simulated error coming from DevTools'
124 + ) {
125 + // Error from forcing an error boundary.
126 + return true;
127 + }
128 +
129 return global._ignoredErrorOrWarningMessages.some(errorOrWarningMessage => {
130 return firstArg.indexOf(errorOrWarningMessage) !== -1;
131 });
packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js
+78
@@ -18,12 +18,14 @@ import {
18 describe('Store component filters', () => {
19 let React;
20 let Types;
21 + let agent;
22 let bridge: FrontendBridge;
23 let store: Store;
24 let utils;
25 let actAsync;
26
27 beforeEach(() => {
28 + agent = global.agent;
29 bridge = global.bridge;
30 store = global.store;
31 store.collapseNodesByDefault = false;
@@ -740,4 +742,80 @@ describe('Store component filters', () => {
742 `);
743 });
744 });
745 +
746 + // @reactVersion >= 16.6
747 + it('resets forced error and fallback states when filters are changed', async () => {
748 + store.componentFilters = [];
749 + class ErrorBoundary extends React.Component {
750 + state = {hasError: false};
751 +
752 + static getDerivedStateFromError() {
753 + return {hasError: true};
754 + }
755 +
756 + render() {
757 + if (this.state.hasError) {
758 + return <div key="did-error" />;
759 + }
760 + return this.props.children;
761 + }
762 + }
763 +
764 + function App() {
765 + return (
766 + <>
767 + <React.Suspense fallback={<div key="loading" />}>
768 + <div key="suspense-content" />
769 + </React.Suspense>
770 + <ErrorBoundary>
771 + <div key="error-content" />
772 + </ErrorBoundary>
773 + </>
774 + );
775 + }
776 +
777 + await actAsync(async () => {
778 + render(<App />);
779 + });
780 + const rendererID = utils.getRendererID();
781 + await actAsync(() => {
782 + agent.overrideSuspense({
783 + id: store.getElementIDAtIndex(2),
784 + rendererID,
785 + forceFallback: true,
786 + });
787 + agent.overrideError({
788 + id: store.getElementIDAtIndex(4),
789 + rendererID,
790 + forceError: true,
791 + });
792 + });
793 +
794 + expect(store).toMatchInlineSnapshot(`
795 + [root]
796 + ▾ <App>
797 + ▾ <Suspense>
798 + <div key="loading">
799 + ▾ <ErrorBoundary>
800 + <div key="did-error">
801 + [suspense-root] rects={[]}
802 + <Suspense name="App" rects={[]}>
803 + `);
804 +
805 + await actAsync(() => {
806 + store.componentFilters = [
807 + utils.createElementTypeFilter(Types.ElementTypeFunction, true),
808 + ];
809 + });
810 +
811 + expect(store).toMatchInlineSnapshot(`
812 + [root]
813 + ▾ <Suspense>
814 + <div key="suspense-content">
815 + ▾ <ErrorBoundary>
816 + <div key="error-content">
817 + [suspense-root] rects={[]}
818 + <Suspense name="Unknown" rects={[]}>
819 + `);
820 + });
821 });
packages/react-devtools-shared/src/backend/fiber/renderer.js
+40
@@ -1512,6 +1512,11 @@ export function attach(
1512 throw Error('Cannot modify filter preferences while profiling');
1513 }
1514
1515 + const previousForcedFallbacks =
1516 + forceFallbackForFibers.size > 0 ? new Set(forceFallbackForFibers) : null;
1517 + const previousForcedErrors =
1518 + forceErrorForFibers.size > 0 ? new Map(forceErrorForFibers) : null;
1519 +
1520 // Recursively unmount all roots.
1521 hook.getFiberRoots(rendererID).forEach(root => {
1522 const rootInstance = rootToFiberInstanceMap.get(root);
@@ -1532,6 +1537,41 @@ export function attach(
1537 // Reset pseudo counters so that new path selections will be persisted.
1538 rootDisplayNameCounter.clear();
1539
1540 + // We just cleared all the forced states. Schedule updates on the affected Fibers
1541 + // so that we get their initial states again according to the new filters.
1542 + if (typeof scheduleUpdate === 'function') {
1543 + if (previousForcedFallbacks !== null) {
1544 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
1545 + for (const fiber of previousForcedFallbacks) {
1546 + if (typeof scheduleRetry === 'function') {
1547 + scheduleRetry(fiber);
1548 + } else {
1549 + scheduleUpdate(fiber);
1550 + }
1551 + }
1552 + }
1553 + if (
1554 + previousForcedErrors !== null &&
1555 + typeof setErrorHandler === 'function'
1556 + ) {
1557 + // Unlike for Suspense, disabling the forced error state requires setting
1558 + // the status to false first. `shouldErrorFiberAccordingToMap` will clear
1559 + // the Fibers later.
1560 + setErrorHandler(shouldErrorFiberAccordingToMap);
1561 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
1562 + for (const [fiber, shouldError] of previousForcedErrors) {
1563 + forceErrorForFibers.set(fiber, false);
1564 + if (shouldError) {
1565 + if (typeof scheduleRetry === 'function') {
1566 + scheduleRetry(fiber);
1567 + } else {
1568 + scheduleUpdate(fiber);
1569 + }
1570 + }
1571 + }
1572 + }
1573 + }
1574 +
1575 // Recursively re-mount all roots with new filter criteria applied.
1576 hook.getFiberRoots(rendererID).forEach(root => {
1577 const current = root.current;