import * as React from 'react';
import {
useContext,
useMemo,
useCallback,
memo,
useState,
useEffect,
} from 'react';
import styles from './HookChangeSummary.css';
import ButtonIcon from '../ButtonIcon';
import {InspectedElementContext} from '../Components/InspectedElementContext';
import {StoreContext} from '../context';
import {
getAlreadyLoadedHookNames,
getHookSourceLocationKey,
} from 'react-devtools-shared/src/hookNamesCache';
import Toggle from '../Toggle';
import type {HooksNode} from 'react-debug-tools/src/ReactDebugHooks';
import type {ChangeDescription} from './types';
const hookListFormatter = new Intl.ListFormat('en', {
style: 'long',
type: 'conjunction',
});
type HookProps = {
hook: HooksNode,
hookNames: Map<string, string> | null,
};
const Hook: React.AbstractComponent<HookProps> = memo(({hook, hookNames}) => {
const hookSource = hook.hookSource;
const hookName = useMemo(() => {
if (!hookSource || !hookNames) return null;
const key = getHookSourceLocationKey(hookSource);
return hookNames.get(key) || null;
}, [hookSource, hookNames]);
return (
<ul className={styles.Hook}>
<li>
{hook.id !== null && (
<span className={styles.PrimitiveHookNumber}>
{String(hook.id + 1)}
</span>
)}
<span
className={hook.id !== null ? styles.PrimitiveHookName : styles.Name}>
{hook.name}
{hookName && <span className={styles.HookName}>({hookName})</span>}
</span>
{hook.subHooks?.map((subHook, index) => (
<Hook key={hook.id} hook={subHook} hookNames={hookNames} />
))}
</li>
</ul>
);
});
const shouldKeepHook = (
hook: HooksNode,
hooksArray: Array<number>,
): boolean => {
if (hook.id !== null && hooksArray.includes(hook.id)) {
return true;
}
const subHooks = hook.subHooks;
if (subHooks == null) {
return false;
}
return subHooks.some(subHook => shouldKeepHook(subHook, hooksArray));
};
const filterHooks = (
hook: HooksNode,
hooksArray: Array<number>,
): HooksNode | null => {
if (!shouldKeepHook(hook, hooksArray)) {
return null;
}
const subHooks = hook.subHooks;
if (subHooks == null) {
return hook;
}
const filteredSubHooks = subHooks
.map(subHook => filterHooks(subHook, hooksArray))
.filter(Boolean);
return filteredSubHooks.length > 0
? {...hook, subHooks: filteredSubHooks}
: hook;
};
type Props = {|
fiberID: number,
hooks: $PropertyType<ChangeDescription, 'hooks'>,
state: $PropertyType<ChangeDescription, 'state'>,
displayMode?: 'detailed' | 'compact',
|};
const HookChangeSummary: React.AbstractComponent<Props> = memo(
({hooks, fiberID, state, displayMode = 'detailed'}: Props) => {
const {parseHookNames, toggleParseHookNames, inspectedElement} = useContext(
InspectedElementContext,
);
const store = useContext(StoreContext);
const [parseHookNamesOptimistic, setParseHookNamesOptimistic] =
useState<boolean>(parseHookNames);
useEffect(() => {
setParseHookNamesOptimistic(parseHookNames);
}, [inspectedElement?.id, parseHookNames]);
const handleOnChange = useCallback(() => {
setParseHookNamesOptimistic(!parseHookNames);
toggleParseHookNames();
}, [toggleParseHookNames, parseHookNames]);
const element = fiberID !== null ? store.getElementByID(fiberID) : null;
const hookNames =
element != null ? getAlreadyLoadedHookNames(element) : null;
const filteredHooks = useMemo(() => {
if (!hooks || !inspectedElement?.hooks) return null;
return inspectedElement.hooks
.map(hook => filterHooks(hook, hooks))
.filter(Boolean);
}, [inspectedElement?.hooks, hooks]);
const hookParsingFailed = parseHookNames && hookNames === null;
if (!hooks?.length) {
return <span>No hooks changed</span>;
}
if (
inspectedElement?.id !== element?.id ||
filteredHooks?.length !== hooks.length ||
displayMode === 'compact'
) {
const hookIds = hooks.map(hookId => String(hookId + 1));
const hookWord = hookIds.length === 1 ? '• Hook' : '• Hooks';
return (
<span>
{hookWord} {hookListFormatter.format(hookIds)} changed
</span>
);
}
let toggleTitle: string;
if (hookParsingFailed) {
toggleTitle = 'Hook parsing failed';
} else if (parseHookNamesOptimistic) {
toggleTitle = 'Parsing hook names ...';
} else {
toggleTitle = 'Parse hook names (may be slow)';
}
if (filteredHooks == null) {
return null;
}
return (
<div>
{filteredHooks.length > 1 ? '• Hooks changed:' : '• Hook changed:'}
{(!parseHookNames || hookParsingFailed) && (
<Toggle
className={
hookParsingFailed
? styles.ToggleError
: styles.LoadHookNamesToggle
}
isChecked={parseHookNamesOptimistic}
isDisabled={parseHookNamesOptimistic || hookParsingFailed}
onChange={handleOnChange}
title={toggleTitle}>
<ButtonIcon type="parse-hook-names" />
</Toggle>
)}
{filteredHooks.map(hook => (
<Hook
key={`${inspectedElement?.id ?? 'unknown'}-${hook.id}`}
hook={hook}
hookNames={hookNames}
/>
))}
</div>
);
},
);
export default HookChangeSummary;