Skip to content

Commit 48068a5

Browse files
Brian Vaughnzhengjitf
Brian Vaughn
authored andcommitted
DevTool: hook names cache no longer loses entries between selection (facebook#21831)
Made several changes to the hooks name cache to avoid losing cached data between selected elements: 1. No longer use React-managed cache. This had the unfortunate side effect of the inspected element cache also clearing the hook names cache. For now, instead, a module-level WeakMap cache is used. This isn't great but we can revisit it later. 2. Hooks are no longer the cache keys (since hook objects get recreated between element inspections). Instead a hook key string made of fileName + line number + column number is used. 3. If hook names have already been loaded for a component, skip showing the load button and just show the hook names by default when selecting the component.
1 parent a08dab8 commit 48068a5

File tree

5 files changed

+67
-47
lines changed

5 files changed

+67
-47
lines changed

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

+6-15
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {SourceMapConsumer} from 'source-map';
1616
import {getHookName, isNonDeclarativePrimitiveHook} from './astUtils';
1717
import {areSourceMapsAppliedToErrors} from './ErrorTester';
1818
import {__DEBUG__} from 'react-devtools-shared/src/constants';
19+
import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
1920

2021
import type {
2122
HooksNode,
@@ -101,17 +102,6 @@ const originalURLToMetadataCache: LRUCache<
101102
},
102103
});
103104

104-
function getLocationKey({
105-
fileName,
106-
lineNumber,
107-
columnNumber,
108-
}: HookSource): string {
109-
if (fileName == null || lineNumber == null || columnNumber == null) {
110-
throw Error('Hook source code location not found.');
111-
}
112-
return `${fileName}:${lineNumber}:${columnNumber}`;
113-
}
114-
115105
export default async function parseHookNames(
116106
hooksTree: HooksTree,
117107
): Thenable<HookNames | null> {
@@ -138,9 +128,9 @@ export default async function parseHookNames(
138128
throw Error('Hook source code location not found.');
139129
}
140130

141-
const locationKey = getLocationKey(hookSource);
131+
const locationKey = getHookSourceLocationKey(hookSource);
142132
if (!locationKeyToHookSourceData.has(locationKey)) {
143-
// Can't be null because getLocationKey() would have thrown
133+
// Can't be null because getHookSourceLocationKey() would have thrown
144134
const runtimeSourceURL = ((hookSource.fileName: any): string);
145135

146136
const hookSourceData: HookSourceData = {
@@ -373,7 +363,7 @@ function findHookNames(
373363
return null; // Should not be reachable.
374364
}
375365

376-
const locationKey = getLocationKey(hookSource);
366+
const locationKey = getHookSourceLocationKey(hookSource);
377367
const hookSourceData = locationKeyToHookSourceData.get(locationKey);
378368
if (!hookSourceData) {
379369
return null; // Should not be reachable.
@@ -426,7 +416,8 @@ function findHookNames(
426416
console.log(`findHookNames() Found name "${name || '-'}"`);
427417
}
428418

429-
map.set(hook, name);
419+
const key = getHookSourceLocationKey(hookSource);
420+
map.set(key, name);
430421
});
431422

432423
return map;

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

+13-7
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ import {
2525
checkForUpdate,
2626
inspectElement,
2727
} from 'react-devtools-shared/src/inspectedElementCache';
28-
import {loadHookNames} from 'react-devtools-shared/src/hookNamesCache';
28+
import {
29+
hasAlreadyLoadedHookNames,
30+
loadHookNames,
31+
} from 'react-devtools-shared/src/hookNamesCache';
2932
import LoadHookNamesFunctionContext from 'react-devtools-shared/src/devtools/views/Components/LoadHookNamesFunctionContext';
3033
import {SettingsContext} from '../Settings/SettingsContext';
3134

@@ -79,16 +82,19 @@ export function InspectedElementContextController({children}: Props) {
7982
path: null,
8083
});
8184

85+
const element =
86+
selectedElementID !== null ? store.getElementByID(selectedElementID) : null;
87+
88+
const alreadyLoadedHookNames =
89+
element != null && hasAlreadyLoadedHookNames(element);
90+
8291
// Parse the currently inspected element's hook names.
8392
// This may be enabled by default (for all elements)
8493
// or it may be opted into on a per-element basis (if it's too slow to be on by default).
8594
const [parseHookNames, setParseHookNames] = useState<boolean>(
86-
parseHookNamesByDefault,
95+
parseHookNamesByDefault || alreadyLoadedHookNames,
8796
);
8897

89-
const element =
90-
selectedElementID !== null ? store.getElementByID(selectedElementID) : null;
91-
9298
const elementHasChanged = element !== null && element !== state.element;
9399

94100
// Reset the cached inspected paths when a new element is selected.
@@ -98,7 +104,7 @@ export function InspectedElementContextController({children}: Props) {
98104
path: null,
99105
});
100106

101-
setParseHookNames(parseHookNamesByDefault);
107+
setParseHookNames(parseHookNamesByDefault || alreadyLoadedHookNames);
102108
}
103109

104110
// Don't load a stale element from the backend; it wastes bridge bandwidth.
@@ -108,7 +114,7 @@ export function InspectedElementContextController({children}: Props) {
108114
inspectedElement = inspectElement(element, state.path, store, bridge);
109115

110116
if (enableHookNameParsing) {
111-
if (parseHookNames) {
117+
if (parseHookNames || alreadyLoadedHookNames) {
112118
if (
113119
inspectedElement !== null &&
114120
inspectedElement.hooks !== null &&

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import Store from '../../store';
2121
import styles from './InspectedElementHooksTree.css';
2222
import useContextMenu from '../../ContextMenu/useContextMenu';
2323
import {meta} from '../../../hydration';
24+
import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
2425
import {
2526
enableHookNameParsing,
2627
enableProfilerChangedHookIndices,
@@ -235,7 +236,11 @@ function HookView({
235236
let displayValue;
236237
let isComplexDisplayValue = false;
237238

238-
const hookName = hookNames != null ? hookNames.get(hook) : null;
239+
const hookSource = hook.hookSource;
240+
const hookName =
241+
hookNames != null && hookSource != null
242+
? hookNames.get(getHookSourceLocationKey(hookSource))
243+
: null;
239244
const hookDisplayName = hookName ? (
240245
<>
241246
{name}

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

+37-21
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,19 @@
77
* @flow
88
*/
99

10-
import {unstable_getCacheForType as getCacheForType} from 'react';
1110
import {enableHookNameParsing} from 'react-devtools-feature-flags';
1211
import {__DEBUG__} from 'react-devtools-shared/src/constants';
1312

1413
import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
1514
import type {Thenable, Wakeable} from 'shared/ReactTypes';
1615
import type {Element} from './devtools/views/Components/types';
17-
import type {HookNames} from 'react-devtools-shared/src/types';
16+
import type {
17+
HookNames,
18+
HookSourceLocationKey,
19+
} from 'react-devtools-shared/src/types';
20+
import type {HookSource} from 'react-debug-tools/src/ReactDebugHooks';
1821

19-
const TIMEOUT = 3000;
22+
const TIMEOUT = 5000;
2023

2124
const Pending = 0;
2225
const Resolved = 1;
@@ -51,14 +54,15 @@ function readRecord<T>(record: Record<T>): ResolvedRecord<T> | RejectedRecord {
5154
}
5255
}
5356

54-
type HookNamesMap = WeakMap<Element, Record<HookNames>>;
57+
// This is intentionally a module-level Map, rather than a React-managed one.
58+
// Otherwise, refreshing the inspected element cache would also clear this cache.
59+
// TODO Rethink this if the React API constraints change.
60+
// See https://github.com/reactwg/react-18/discussions/25#discussioncomment-980435
61+
const map: WeakMap<Element, Record<HookNames>> = new WeakMap();
5562

56-
function createMap(): HookNamesMap {
57-
return new WeakMap();
58-
}
59-
60-
function getRecordMap(): WeakMap<Element, Record<HookNames>> {
61-
return getCacheForType(createMap);
63+
export function hasAlreadyLoadedHookNames(element: Element): boolean {
64+
const record = map.get(element);
65+
return record != null && record.status === Resolved;
6266
}
6367

6468
export function loadHookNames(
@@ -70,14 +74,15 @@ export function loadHookNames(
7074
return null;
7175
}
7276

73-
const map = getRecordMap();
74-
7577
let record = map.get(element);
76-
if (record) {
77-
// TODO Do we need to update the Map to use new the hooks list objects as keys
78-
// or will these be stable between inspections as a component updates?
79-
// It seems like they're stable.
80-
} else {
78+
79+
if (__DEBUG__) {
80+
console.groupCollapsed('loadHookNames() record:');
81+
console.log(record);
82+
console.groupEnd();
83+
}
84+
85+
if (!record) {
8186
const callbacks = new Set();
8287
const wakeable: Wakeable = {
8388
then(callback) {
@@ -126,14 +131,14 @@ export function loadHookNames(
126131
wake();
127132
},
128133
function onError(error) {
129-
if (__DEBUG__) {
130-
console.log('[hookNamesCache] onError() error:', error);
131-
}
132-
133134
if (didTimeout) {
134135
return;
135136
}
136137

138+
if (__DEBUG__) {
139+
console.log('[hookNamesCache] onError() error:', error);
140+
}
141+
137142
const thrownRecord = ((newRecord: any): RejectedRecord);
138143
thrownRecord.status = Rejected;
139144
thrownRecord.value = null;
@@ -165,3 +170,14 @@ export function loadHookNames(
165170
const response = readRecord(record).value;
166171
return response;
167172
}
173+
174+
export function getHookSourceLocationKey({
175+
fileName,
176+
lineNumber,
177+
columnNumber,
178+
}: HookSource): HookSourceLocationKey {
179+
if (fileName == null || lineNumber == null || columnNumber == null) {
180+
throw Error('Hook source code location not found.');
181+
}
182+
return `${fileName}:${lineNumber}:${columnNumber}`;
183+
}

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
* @flow
88
*/
99

10-
import type {HooksNode} from 'react-debug-tools/src/ReactDebugHooks';
11-
1210
export type Wall = {|
1311
// `listen` returns the "unlisten" function.
1412
listen: (fn: Function) => Function,
@@ -80,7 +78,11 @@ export type ComponentFilter =
8078
| RegExpComponentFilter;
8179

8280
export type HookName = string | null;
83-
export type HookNames = Map<HooksNode, HookName>;
81+
// Map of hook source ("<filename>:<line-number>:<column-number>") to name.
82+
// Hook source is used instead of the hook itself becuase the latter is not stable between element inspections.
83+
// We use a Map rather than an Array because of nested hooks and traversal ordering.
84+
export type HookSourceLocationKey = string;
85+
export type HookNames = Map<HookSourceLocationKey, HookName>;
8486

8587
export type LRUCache<K, V> = {|
8688
get: (key: K) => V,

0 commit comments

Comments
 (0)