Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/react-devtools-shared/src/__tests__/setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 <div key="did-error" />;
}
return this.props.children;
}
}

function App() {
return (
<>
<React.Suspense fallback={<div key="loading" />}>
<div key="suspense-content" />
</React.Suspense>
<ErrorBoundary>
<div key="error-content" />
</ErrorBoundary>
</>
);
}

await actAsync(async () => {
render(<App />);
});
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]
▾ <App>
▾ <Suspense>
<div key="loading">
▾ <ErrorBoundary>
<div key="did-error">
[suspense-root] rects={[]}
<Suspense name="App" rects={[]}>
`);

await actAsync(() => {
store.componentFilters = [
utils.createElementTypeFilter(Types.ElementTypeFunction, true),
];
});

expect(store).toMatchInlineSnapshot(`
[root]
▾ <Suspense>
<div key="suspense-content">
▾ <ErrorBoundary>
<div key="error-content">
[suspense-root] rects={[]}
<Suspense name="Unknown" rects={[]}>
`);
});
});
40 changes: 40 additions & 0 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1510,6 +1510,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);
Expand All @@ -1530,6 +1535,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;
Expand Down
Loading