import type {ReactContext} from 'shared/ReactTypes';
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 {
inspectElement,
startElementUpdatesPolling,
} 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 type {HookNames} from 'react-devtools-shared/src/frontend/types';
import type {ReactNodeList} from 'shared/ReactTypes';
import type {
Element,
InspectedElement,
} from 'react-devtools-shared/src/frontend/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: ReactContext<Context> =
createContext<Context>(((null: any): Context));
export type Props = {
children: ReactNodeList,
};
export function InspectedElementContextController({
children,
}: Props): React.Node {
const {inspectedElementID} = useContext(TreeStateContext);
const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
const {parseHookNames: parseHookNamesByDefault} = useContext(SettingsContext);
const hookNamesModuleLoader = useContext(HookNamesModuleLoaderContext);
const refresh = useCacheRefresh();
const [state, setState] = useState<{
element: Element | null,
path: Array<number | string> | null,
}>({
element: null,
path: null,
});
const element =
inspectedElementID !== null
? store.getElementByID(inspectedElementID)
: null;
const alreadyLoadedHookNames =
element != null && hasAlreadyLoadedHookNames(element);
const [parseHookNames, setParseHookNames] = useState<boolean>(
parseHookNamesByDefault || alreadyLoadedHookNames,
);
const [bridgeIsAlive, setBridgeIsAliveStatus] = useState<boolean>(true);
const elementHasChanged = element !== null && element !== state.element;
if (elementHasChanged) {
setState({
element,
path: null,
});
setParseHookNames(parseHookNamesByDefault || alreadyLoadedHookNames);
}
const purgeCachedMetadataRef = useRef(null);
let hookNames: HookNames | null = null;
let inspectedElement = null;
if (!elementHasChanged && element !== null) {
inspectedElement = inspectElement(element, state.path, store, bridge);
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],
);
useEffect(() => {
const purgeCachedMetadata = purgeCachedMetadataRef.current;
if (typeof purgeCachedMetadata === 'function') {
const fastRefreshScheduled = () => {
startTransition(() => {
clearHookNamesCache();
purgeCachedMetadata();
refresh();
});
};
bridge.addListener('fastRefreshScheduled', fastRefreshScheduled);
return () =>
bridge.removeListener('fastRefreshScheduled', fastRefreshScheduled);
}
}, [bridge]);
useEffect(() => {
if (state.path !== null) {
setState({
element: state.element,
path: null,
});
}
}, [state]);
useEffect(() => {
setBridgeIsAliveStatus(true);
const listener = () => setBridgeIsAliveStatus(false);
bridge.addListener('shutdown', listener);
return () => bridge.removeListener('shutdown', listener);
}, [bridge]);
useEffect(() => {
if (element !== null && bridgeIsAlive) {
const {abort, pause, resume} = startElementUpdatesPolling({
bridge,
element,
refresh,
store,
});
bridge.addListener('resumeElementPolling', resume);
bridge.addListener('pauseElementPolling', pause);
return () => {
bridge.removeListener('resumeElementPolling', resume);
bridge.removeListener('pauseElementPolling', pause);
abort();
};
}
}, [
element,
hookNames,
inspectedElement,
state,
bridgeIsAlive,
]);
const value = useMemo<Context>(
() => ({
hookNames,
inspectedElement,
inspectPaths,
parseHookNames,
toggleParseHookNames,
}),
[
hookNames,
inspectedElement,
inspectPaths,
parseHookNames,
toggleParseHookNames,
],
);
return (
<InspectedElementContext.Provider value={value}>
{children}
</InspectedElementContext.Provider>
);
}