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: any): Context));
    
  61. 
    
  62. export type Props = {
    
  63.   children: ReactNodeList,
    
  64. };
    
  65. 
    
  66. export function InspectedElementContextController({
    
  67.   children,
    
  68. }: Props): React.Node {
    
  69.   const {selectedElementID} = 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.     selectedElementID !== null ? store.getElementByID(selectedElementID) : null;
    
  97. 
    
  98.   const alreadyLoadedHookNames =
    
  99.     element != null && hasAlreadyLoadedHookNames(element);
    
  100. 
    
  101.   // Parse the currently inspected element's hook names.
    
  102.   // This may be enabled by default (for all elements)
    
  103.   // or it may be opted into on a per-element basis (if it's too slow to be on by default).
    
  104.   const [parseHookNames, setParseHookNames] = useState<boolean>(
    
  105.     parseHookNamesByDefault || alreadyLoadedHookNames,
    
  106.   );
    
  107. 
    
  108.   const [bridgeIsAlive, setBridgeIsAliveStatus] = useState<boolean>(true);
    
  109. 
    
  110.   const elementHasChanged = element !== null && element !== state.element;
    
  111. 
    
  112.   // Reset the cached inspected paths when a new element is selected.
    
  113.   if (elementHasChanged) {
    
  114.     setState({
    
  115.       element,
    
  116.       path: null,
    
  117.     });
    
  118. 
    
  119.     setParseHookNames(parseHookNamesByDefault || alreadyLoadedHookNames);
    
  120.   }
    
  121. 
    
  122.   const purgeCachedMetadataRef = useRef(null);
    
  123. 
    
  124.   // Don't load a stale element from the backend; it wastes bridge bandwidth.
    
  125.   let hookNames: HookNames | null = null;
    
  126.   let inspectedElement = null;
    
  127.   if (!elementHasChanged && element !== null) {
    
  128.     inspectedElement = inspectElement(element, state.path, store, bridge);
    
  129. 
    
  130.     if (typeof hookNamesModuleLoader === 'function') {
    
  131.       if (parseHookNames || alreadyLoadedHookNames) {
    
  132.         const hookNamesModule = loadModule(hookNamesModuleLoader);
    
  133.         if (hookNamesModule !== null) {
    
  134.           const {parseHookNames: loadHookNamesFunction, purgeCachedMetadata} =
    
  135.             hookNamesModule;
    
  136. 
    
  137.           purgeCachedMetadataRef.current = purgeCachedMetadata;
    
  138. 
    
  139.           if (
    
  140.             inspectedElement !== null &&
    
  141.             inspectedElement.hooks !== null &&
    
  142.             loadHookNamesFunction !== null
    
  143.           ) {
    
  144.             hookNames = loadHookNames(
    
  145.               element,
    
  146.               inspectedElement.hooks,
    
  147.               loadHookNamesFunction,
    
  148.               fetchFileWithCaching,
    
  149.             );
    
  150.           }
    
  151.         }
    
  152.       }
    
  153.     }
    
  154.   }
    
  155. 
    
  156.   const toggleParseHookNames: ToggleParseHookNames =
    
  157.     useCallback<ToggleParseHookNames>(() => {
    
  158.       startTransition(() => {
    
  159.         setParseHookNames(value => !value);
    
  160.         refresh();
    
  161.       });
    
  162.     }, [setParseHookNames]);
    
  163. 
    
  164.   const inspectPaths: InspectPathFunction = useCallback<InspectPathFunction>(
    
  165.     (path: Path) => {
    
  166.       startTransition(() => {
    
  167.         setState({
    
  168.           element: state.element,
    
  169.           path,
    
  170.         });
    
  171.         refresh();
    
  172.       });
    
  173.     },
    
  174.     [setState, state],
    
  175.   );
    
  176. 
    
  177.   const inspectedElementRef = useRef<null | InspectedElement>(null);
    
  178.   useEffect(() => {
    
  179.     if (
    
  180.       inspectedElement !== null &&
    
  181.       inspectedElement.hooks !== null &&
    
  182.       inspectedElementRef.current !== inspectedElement
    
  183.     ) {
    
  184.       inspectedElementRef.current = inspectedElement;
    
  185.     }
    
  186.   }, [inspectedElement]);
    
  187. 
    
  188.   useEffect(() => {
    
  189.     const purgeCachedMetadata = purgeCachedMetadataRef.current;
    
  190.     if (typeof purgeCachedMetadata === 'function') {
    
  191.       // When Fast Refresh updates a component, any cached AST metadata may be invalid.
    
  192.       const fastRefreshScheduled = () => {
    
  193.         startTransition(() => {
    
  194.           clearHookNamesCache();
    
  195.           purgeCachedMetadata();
    
  196.           refresh();
    
  197.         });
    
  198.       };
    
  199.       bridge.addListener('fastRefreshScheduled', fastRefreshScheduled);
    
  200.       return () =>
    
  201.         bridge.removeListener('fastRefreshScheduled', fastRefreshScheduled);
    
  202.     }
    
  203.   }, [bridge]);
    
  204. 
    
  205.   // Reset path now that we've asked the backend to hydrate it.
    
  206.   // The backend is stateful, so we don't need to remember this path the next time we inspect.
    
  207.   useEffect(() => {
    
  208.     if (state.path !== null) {
    
  209.       setState({
    
  210.         element: state.element,
    
  211.         path: null,
    
  212.       });
    
  213.     }
    
  214.   }, [state]);
    
  215. 
    
  216.   useEffect(() => {
    
  217.     // Assuming that new bridge is always alive at this moment
    
  218.     setBridgeIsAliveStatus(true);
    
  219. 
    
  220.     const listener = () => setBridgeIsAliveStatus(false);
    
  221.     bridge.addListener('shutdown', listener);
    
  222. 
    
  223.     return () => bridge.removeListener('shutdown', listener);
    
  224.   }, [bridge]);
    
  225. 
    
  226.   // Periodically poll the selected element for updates.
    
  227.   useEffect(() => {
    
  228.     if (element !== null && bridgeIsAlive) {
    
  229.       const {abort, pause, resume} = startElementUpdatesPolling({
    
  230.         bridge,
    
  231.         element,
    
  232.         refresh,
    
  233.         store,
    
  234.       });
    
  235. 
    
  236.       bridge.addListener('resumeElementPolling', resume);
    
  237.       bridge.addListener('pauseElementPolling', pause);
    
  238. 
    
  239.       return () => {
    
  240.         bridge.removeListener('resumeElementPolling', resume);
    
  241.         bridge.removeListener('pauseElementPolling', pause);
    
  242. 
    
  243.         abort();
    
  244.       };
    
  245.     }
    
  246.   }, [
    
  247.     element,
    
  248.     hookNames,
    
  249.     // Reset this timer any time the element we're inspecting gets a new response.
    
  250.     // No sense to ping right away after e.g. inspecting/hydrating a path.
    
  251.     inspectedElement,
    
  252.     state,
    
  253.     bridgeIsAlive,
    
  254.   ]);
    
  255. 
    
  256.   const value = useMemo<Context>(
    
  257.     () => ({
    
  258.       hookNames,
    
  259.       inspectedElement,
    
  260.       inspectPaths,
    
  261.       parseHookNames,
    
  262.       toggleParseHookNames,
    
  263.     }),
    
  264.     [
    
  265.       hookNames,
    
  266.       inspectedElement,
    
  267.       inspectPaths,
    
  268.       parseHookNames,
    
  269.       toggleParseHookNames,
    
  270.     ],
    
  271.   );
    
  272. 
    
  273.   return (
    
  274.     <InspectedElementContext.Provider value={value}>
    
  275.       {children}
    
  276.     </InspectedElementContext.Provider>
    
  277.   );
    
  278. }