Skip to content

Commit e26cb8f

Browse files
author
Brian Vaughn
authored
Clear named hooks Suspense and AST cache after a Fast Refresh (#21891)
1 parent 682bbd0 commit e26cb8f

File tree

14 files changed

+109
-24
lines changed

14 files changed

+109
-24
lines changed

packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe('parseHookNames', () => {
3333

3434
inspectHooks = require('react-debug-tools/src/ReactDebugHooks')
3535
.inspectHooks;
36-
parseHookNames = require('../parseHookNames').default;
36+
parseHookNames = require('../parseHookNames').parseHookNames;
3737

3838
// Jest (jest-runner?) configures Errors to automatically account for source maps.
3939
// This changes behavior between our tests and the browser.
@@ -158,6 +158,10 @@ describe('parseHookNames', () => {
158158
]);
159159
});
160160

161+
// TODO Test that cache purge works
162+
163+
// TODO Test that cached metadata is purged when Fast Refresh scheduled
164+
161165
describe('inline, external and bundle source maps', () => {
162166
it('should work for simple components', async () => {
163167
async function test(path, name = 'Component') {

packages/react-devtools-extensions/src/main.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
getSavedComponentFilters,
1313
getShowInlineWarningsAndErrors,
1414
} from 'react-devtools-shared/src/utils';
15-
import parseHookNames from './parseHookNames';
15+
import {parseHookNames, purgeCachedMetadata} from './parseHookNames';
1616
import {
1717
localStorageGetItem,
1818
localStorageRemoveItem,
@@ -215,9 +215,10 @@ function createPanelIfReactLoaded() {
215215
browserTheme: getBrowserTheme(),
216216
componentsPortalContainer,
217217
enabledInspectedElementContextMenu: true,
218-
loadHookNamesFunction: parseHookNames,
218+
loadHookNames: parseHookNames,
219219
overrideTab,
220220
profilerPortalContainer,
221+
purgeCachedHookNamesMetadata: purgeCachedMetadata,
221222
showTabBar: false,
222223
store,
223224
warnIfUnsupportedVersionDetected: true,

packages/react-devtools-extensions/src/parseHookNames.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ const originalURLToMetadataCache: LRUCache<
102102
},
103103
});
104104

105-
export default async function parseHookNames(
105+
export async function parseHookNames(
106106
hooksTree: HooksTree,
107107
): Thenable<HookNames | null> {
108108
if (!enableHookNameParsing) {
@@ -623,3 +623,8 @@ function updateLruCache(
623623
});
624624
return Promise.resolve();
625625
}
626+
627+
export function purgeCachedMetadata(): void {
628+
originalURLToMetadataCache.reset();
629+
runtimeURLToMetadataCache.reset();
630+
}

packages/react-devtools-shared/src/backend/agent.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,14 @@ export default class Agent extends EventEmitter<{|
690690
this.emit('traceUpdates', nodes);
691691
};
692692

693+
onFastRefreshScheduled = () => {
694+
if (__DEBUG__) {
695+
debug('onFastRefreshScheduled');
696+
}
697+
698+
this._bridge.send('fastRefreshScheduled');
699+
};
700+
693701
onHookOperations = (operations: Array<number>) => {
694702
if (__DEBUG__) {
695703
debug(

packages/react-devtools-shared/src/backend/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export function initBackend(
4848
agent.onUnsupportedRenderer(id);
4949
}),
5050

51+
hook.sub('fastRefreshScheduled', agent.onFastRefreshScheduled),
5152
hook.sub('operations', agent.onHookOperations),
5253
hook.sub('traceUpdates', agent.onTraceUpdates),
5354

packages/react-devtools-shared/src/backend/renderer.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,7 @@ export function attach(
568568
overrideProps,
569569
overridePropsDeletePath,
570570
overridePropsRenamePath,
571+
scheduleRefresh,
571572
setErrorHandler,
572573
setSuspenseHandler,
573574
scheduleUpdate,
@@ -579,6 +580,22 @@ export function attach(
579580
typeof setSuspenseHandler === 'function' &&
580581
typeof scheduleUpdate === 'function';
581582

583+
if (typeof scheduleRefresh === 'function') {
584+
// When Fast Refresh updates a component, the frontend may need to purge cached information.
585+
// For example, ASTs cached for the component (for named hooks) may no longer be valid.
586+
// Send a signal to the frontend to purge this cached information.
587+
// The "fastRefreshScheduled" dispatched is global (not Fiber or even Renderer specific).
588+
// This is less effecient since it means the front-end will need to purge the entire cache,
589+
// but this is probably an okay trade off in order to reduce coupling between the DevTools and Fast Refresh.
590+
renderer.scheduleRefresh = (...args) => {
591+
try {
592+
hook.emit('fastRefreshScheduled');
593+
} finally {
594+
return scheduleRefresh(...args);
595+
}
596+
};
597+
}
598+
582599
// Tracks Fibers with recently changed number of error/warning messages.
583600
// These collections store the Fiber rather than the ID,
584601
// in order to avoid generating an ID for Fibers that never get mounted

packages/react-devtools-shared/src/backend/types.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ export type ReactRenderer = {
144144
Mount?: any,
145145
// Only injected by React v17.0.3+ in DEV mode
146146
setErrorHandler?: ?(shouldError: (fiber: Object) => ?boolean) => void,
147+
// Intentionally opaque type to avoid coupling DevTools to different Fast Refresh versions.
148+
scheduleRefresh?: Function,
147149
...
148150
};
149151

packages/react-devtools-shared/src/bridge.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ type UpdateConsolePatchSettingsParams = {|
169169
export type BackendEvents = {|
170170
bridgeProtocol: [BridgeProtocol],
171171
extensionBackendInitialized: [],
172+
fastRefreshScheduled: [],
172173
inspectedElement: [InspectedElementPayload],
173174
isBackendStorageAPISupported: [boolean],
174175
isSynchronousXHRSupported: [boolean],
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// @flow
2+
3+
import {createContext} from 'react';
4+
import type {
5+
LoadHookNamesFunction,
6+
PurgeCachedHookNamesMetadata,
7+
} from '../DevTools';
8+
9+
export type Context = {
10+
loadHookNames: LoadHookNamesFunction | null,
11+
purgeCachedMetadata: PurgeCachedHookNamesMetadata | null,
12+
};
13+
14+
const HookNamesContext = createContext<Context>({
15+
loadHookNames: null,
16+
purgeCachedMetadata: null,
17+
});
18+
HookNamesContext.displayName = 'HookNamesContext';
19+
20+
export default HookNamesContext;

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ import {
2626
inspectElement,
2727
} from 'react-devtools-shared/src/inspectedElementCache';
2828
import {
29+
clearHookNamesCache,
2930
hasAlreadyLoadedHookNames,
3031
loadHookNames,
3132
} from 'react-devtools-shared/src/hookNamesCache';
32-
import LoadHookNamesFunctionContext from 'react-devtools-shared/src/devtools/views/Components/LoadHookNamesFunctionContext';
33+
import HookNamesContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesContext';
3334
import {SettingsContext} from '../Settings/SettingsContext';
3435

3536
import type {HookNames} from 'react-devtools-shared/src/types';
@@ -63,7 +64,10 @@ export type Props = {|
6364

6465
export function InspectedElementContextController({children}: Props) {
6566
const {selectedElementID} = useContext(TreeStateContext);
66-
const loadHookNamesFunction = useContext(LoadHookNamesFunctionContext);
67+
const {
68+
loadHookNames: loadHookNamesFunction,
69+
purgeCachedMetadata,
70+
} = useContext(HookNamesContext);
6771
const bridge = useContext(BridgeContext);
6872
const store = useContext(StoreContext);
6973
const {parseHookNames: parseHookNamesByDefault} = useContext(SettingsContext);
@@ -150,6 +154,24 @@ export function InspectedElementContextController({children}: Props) {
150154
[setState, state],
151155
);
152156

157+
useEffect(() => {
158+
if (enableHookNameParsing) {
159+
if (typeof purgeCachedMetadata === 'function') {
160+
// When Fast Refresh updates a component, any cached AST metadata may be invalid.
161+
const fastRefreshScheduled = () => {
162+
startTransition(() => {
163+
clearHookNamesCache();
164+
purgeCachedMetadata();
165+
refresh();
166+
});
167+
};
168+
bridge.addListener('fastRefreshScheduled', fastRefreshScheduled);
169+
return () =>
170+
bridge.removeListener('fastRefreshScheduled', fastRefreshScheduled);
171+
}
172+
}
173+
}, [bridge]);
174+
153175
// Reset path now that we've asked the backend to hydrate it.
154176
// The backend is stateful, so we don't need to remember this path the next time we inspect.
155177
useEffect(() => {

0 commit comments

Comments
 (0)