forked from facebook/react
-
-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathInspectedElementContext.js
259 lines (229 loc) · 7.82 KB
/
InspectedElementContext.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import {
createContext,
startTransition,
unstable_useCacheRefresh as useCacheRefresh,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {TreeStateContext} from './TreeContext';
import {BridgeContext, StoreContext} from '../context';
import {
checkForUpdate,
inspectElement,
} from 'react-devtools-shared/src/inspectedElementCache';
import {
clearHookNamesCache,
hasAlreadyLoadedHookNames,
loadHookNames,
} from 'react-devtools-shared/src/hookNamesCache';
import {loadModule} from 'react-devtools-shared/src/dynamicImportCache';
import FetchFileWithCachingContext from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext';
import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
import {SettingsContext} from '../Settings/SettingsContext';
import {enableNamedHooksFeature} from 'react-devtools-feature-flags';
import type {HookNames} from 'react-devtools-shared/src/types';
import type {ReactNodeList} from 'shared/ReactTypes';
import type {
Element,
InspectedElement,
} from 'react-devtools-shared/src/devtools/views/Components/types';
type Path = Array<string | number>;
type InspectPathFunction = (path: Path) => void;
export type ToggleParseHookNames = () => void;
type Context = {|
hookNames: HookNames | null,
inspectedElement: InspectedElement | null,
inspectPaths: InspectPathFunction,
parseHookNames: boolean,
toggleParseHookNames: ToggleParseHookNames,
|};
export const InspectedElementContext = createContext<Context>(
((null: any): Context),
);
const POLL_INTERVAL = 1000;
export type Props = {|
children: ReactNodeList,
|};
export function InspectedElementContextController({children}: Props) {
const {selectedElementID} = useContext(TreeStateContext);
const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
const {parseHookNames: parseHookNamesByDefault} = useContext(SettingsContext);
// parseHookNames has a lot of code.
// Embedding it into a build makes the build large.
// This function enables DevTools to make use of Suspense to lazily import() it only if the feature will be used.
const hookNamesModuleLoader = useContext(HookNamesModuleLoaderContext);
const refresh = useCacheRefresh();
// Temporarily stores most recently-inspected (hydrated) path.
// The transition that updates this causes the component to re-render and ask the cache->backend for the new path.
// When a path is sent along with an "inspectElement" request,
// the backend knows to send its dehydrated data even if the element hasn't updated since the last request.
const [state, setState] = useState<{|
element: Element | null,
path: Array<number | string> | null,
|}>({
element: null,
path: null,
});
const element =
selectedElementID !== null ? store.getElementByID(selectedElementID) : null;
const alreadyLoadedHookNames =
element != null && hasAlreadyLoadedHookNames(element);
// Parse the currently inspected element's hook names.
// This may be enabled by default (for all elements)
// or it may be opted into on a per-element basis (if it's too slow to be on by default).
const [parseHookNames, setParseHookNames] = useState<boolean>(
parseHookNamesByDefault || alreadyLoadedHookNames,
);
const elementHasChanged = element !== null && element !== state.element;
// Reset the cached inspected paths when a new element is selected.
if (elementHasChanged) {
setState({
element,
path: null,
});
setParseHookNames(parseHookNamesByDefault || alreadyLoadedHookNames);
}
const purgeCachedMetadataRef = useRef(null);
// Don't load a stale element from the backend; it wastes bridge bandwidth.
let hookNames: HookNames | null = null;
let inspectedElement = null;
if (!elementHasChanged && element !== null) {
inspectedElement = inspectElement(element, state.path, store, bridge);
if (enableNamedHooksFeature) {
if (typeof hookNamesModuleLoader === 'function') {
if (parseHookNames || alreadyLoadedHookNames) {
const hookNamesModule = loadModule(hookNamesModuleLoader);
if (hookNamesModule !== null) {
const {
parseHookNames: loadHookNamesFunction,
purgeCachedMetadata,
} = hookNamesModule;
purgeCachedMetadataRef.current = purgeCachedMetadata;
if (
inspectedElement !== null &&
inspectedElement.hooks !== null &&
loadHookNamesFunction !== null
) {
hookNames = loadHookNames(
element,
inspectedElement.hooks,
loadHookNamesFunction,
fetchFileWithCaching,
);
}
}
}
}
}
}
const toggleParseHookNames: ToggleParseHookNames = useCallback<ToggleParseHookNames>(() => {
startTransition(() => {
setParseHookNames(value => !value);
refresh();
});
}, [setParseHookNames]);
const inspectPaths: InspectPathFunction = useCallback<InspectPathFunction>(
(path: Path) => {
startTransition(() => {
setState({
element: state.element,
path,
});
refresh();
});
},
[setState, state],
);
const inspectedElementRef = useRef(null);
useEffect(() => {
if (
inspectedElement !== null &&
inspectedElement.hooks !== null &&
inspectedElementRef.current !== inspectedElement
) {
inspectedElementRef.current = inspectedElement;
}
}, [inspectedElement]);
useEffect(() => {
const purgeCachedMetadata = purgeCachedMetadataRef.current;
if (typeof purgeCachedMetadata === 'function') {
// When Fast Refresh updates a component, any cached AST metadata may be invalid.
const fastRefreshScheduled = () => {
startTransition(() => {
clearHookNamesCache();
purgeCachedMetadata();
refresh();
});
};
bridge.addListener('fastRefreshScheduled', fastRefreshScheduled);
return () =>
bridge.removeListener('fastRefreshScheduled', fastRefreshScheduled);
}
}, [bridge]);
// Reset path now that we've asked the backend to hydrate it.
// The backend is stateful, so we don't need to remember this path the next time we inspect.
useEffect(() => {
if (state.path !== null) {
setState({
element: state.element,
path: null,
});
}
}, [state]);
// Periodically poll the selected element for updates.
useEffect(() => {
if (element !== null) {
const checkForUpdateWrapper = () => {
checkForUpdate({bridge, element, refresh, store});
timeoutID = setTimeout(checkForUpdateWrapper, POLL_INTERVAL);
};
let timeoutID = setTimeout(checkForUpdateWrapper, POLL_INTERVAL);
return () => {
clearTimeout(timeoutID);
};
}
}, [
element,
hookNames,
// Reset this timer any time the element we're inspecting gets a new response.
// No sense to ping right away after e.g. inspecting/hydrating a path.
inspectedElement,
state,
]);
const value = useMemo<Context>(
() => ({
hookNames,
inspectedElement,
inspectPaths,
parseHookNames,
toggleParseHookNames,
}),
[
hookNames,
inspectedElement,
inspectPaths,
parseHookNames,
toggleParseHookNames,
],
);
return (
<InspectedElementContext.Provider value={value}>
{children}
</InspectedElementContext.Provider>
);
}