diff --git a/packages/react-devtools-shared/src/__tests__/setupTests.js b/packages/react-devtools-shared/src/__tests__/setupTests.js index 610191fb2cdb8..aad3fbc00eaae 100644 --- a/packages/react-devtools-shared/src/__tests__/setupTests.js +++ b/packages/react-devtools-shared/src/__tests__/setupTests.js @@ -116,6 +116,16 @@ function shouldIgnoreConsoleErrorOrWarn(args) { return false; } + const maybeError = args[1]; + if ( + maybeError !== null && + typeof maybeError === 'object' && + maybeError.message === 'Simulated error coming from DevTools' + ) { + // Error from forcing an error boundary. + return true; + } + return global._ignoredErrorOrWarningMessages.some(errorOrWarningMessage => { return firstArg.indexOf(errorOrWarningMessage) !== -1; }); diff --git a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js index 439315dc85836..ce423e94895bb 100644 --- a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js @@ -18,12 +18,14 @@ import { describe('Store component filters', () => { let React; let Types; + let agent; let bridge: FrontendBridge; let store: Store; let utils; let actAsync; beforeEach(() => { + agent = global.agent; bridge = global.bridge; store = global.store; store.collapseNodesByDefault = false; @@ -740,4 +742,80 @@ describe('Store component filters', () => { `); }); }); + + // @reactVersion >= 16.6 + it('resets forced error and fallback states when filters are changed', async () => { + store.componentFilters = []; + class ErrorBoundary extends React.Component { + state = {hasError: false}; + + static getDerivedStateFromError() { + return {hasError: true}; + } + + render() { + if (this.state.hasError) { + return
; + } + return this.props.children; + } + } + + function App() { + return ( + <> + }> +
+ + +
+ + + ); + } + + await actAsync(async () => { + render(); + }); + const rendererID = utils.getRendererID(); + await actAsync(() => { + agent.overrideSuspense({ + id: store.getElementIDAtIndex(2), + rendererID, + forceFallback: true, + }); + agent.overrideError({ + id: store.getElementIDAtIndex(4), + rendererID, + forceError: true, + }); + }); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + ▾ +
+ ▾ +
+ [suspense-root] rects={[]} + + `); + + await actAsync(() => { + store.componentFilters = [ + utils.createElementTypeFilter(Types.ElementTypeFunction, true), + ]; + }); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ +
+ ▾ +
+ [suspense-root] rects={[]} + + `); + }); }); diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 8cdc731058f83..ddafd014e7ff6 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -1512,6 +1512,11 @@ export function attach( throw Error('Cannot modify filter preferences while profiling'); } + const previousForcedFallbacks = + forceFallbackForFibers.size > 0 ? new Set(forceFallbackForFibers) : null; + const previousForcedErrors = + forceErrorForFibers.size > 0 ? new Map(forceErrorForFibers) : null; + // Recursively unmount all roots. hook.getFiberRoots(rendererID).forEach(root => { const rootInstance = rootToFiberInstanceMap.get(root); @@ -1532,6 +1537,41 @@ export function attach( // Reset pseudo counters so that new path selections will be persisted. rootDisplayNameCounter.clear(); + // We just cleared all the forced states. Schedule updates on the affected Fibers + // so that we get their initial states again according to the new filters. + if (typeof scheduleUpdate === 'function') { + if (previousForcedFallbacks !== null) { + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const fiber of previousForcedFallbacks) { + if (typeof scheduleRetry === 'function') { + scheduleRetry(fiber); + } else { + scheduleUpdate(fiber); + } + } + } + if ( + previousForcedErrors !== null && + typeof setErrorHandler === 'function' + ) { + // Unlike for Suspense, disabling the forced error state requires setting + // the status to false first. `shouldErrorFiberAccordingToMap` will clear + // the Fibers later. + setErrorHandler(shouldErrorFiberAccordingToMap); + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const [fiber, shouldError] of previousForcedErrors) { + forceErrorForFibers.set(fiber, false); + if (shouldError) { + if (typeof scheduleRetry === 'function') { + scheduleRetry(fiber); + } else { + scheduleUpdate(fiber); + } + } + } + } + } + // Recursively re-mount all roots with new filter criteria applied. hook.getFiberRoots(rendererID).forEach(root => { const current = root.current;