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