diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Components.js b/packages/react-devtools-shared/src/devtools/views/Components/Components.js index 995f1d92cf323..b7ce607685051 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Components.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Components.js @@ -19,7 +19,6 @@ import { } from 'react-devtools-shared/src/storage'; import InspectedElementErrorBoundary from './InspectedElementErrorBoundary'; import InspectedElement from './InspectedElement'; -import {InspectedElementContextController} from './InspectedElementContext'; import {ModalDialog} from '../ModalDialog'; import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal'; import {NativeStyleContextController} from './NativeStyleEditor/context'; @@ -162,9 +161,7 @@ function Components(_: {}) {
- - - +
diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index 9bee64ff95150..bd14bdda0f5ba 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -28,6 +28,7 @@ import {SettingsContextController} from './Settings/SettingsContext'; import {TreeContextController} from './Components/TreeContext'; import ViewElementSourceContext from './Components/ViewElementSourceContext'; import FetchFileWithCachingContext from './Components/FetchFileWithCachingContext'; +import {InspectedElementContextController} from './Components/InspectedElementContext'; import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext'; import {ProfilerContextController} from './Profiler/ProfilerContext'; import {TimelineContextController} from 'react-devtools-timeline/src/TimelineContext'; @@ -276,43 +277,47 @@ export default function DevTools({ - -
- {showTabBar && ( -
- - - {process.env.DEVTOOLS_VERSION} - -
- + +
+ {showTabBar && ( +
+ + + {process.env.DEVTOOLS_VERSION} + +
+ +
+ )} + + - )} - - -
- + + diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/HookChangeSummary.css b/packages/react-devtools-shared/src/devtools/views/Profiler/HookChangeSummary.css new file mode 100644 index 0000000000000..d1aff47bddfcf --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/HookChangeSummary.css @@ -0,0 +1,59 @@ +.LoadHookNamesToggle, +.ToggleError { + padding: 2px; + background: none; + border: none; + cursor: pointer; + position: relative; + bottom: -0.2em; + margin-block: -1em; +} + +.ToggleError { + color: var(--color-error-text); +} + +.Hook { + list-style-type: none; + margin: 0; + padding-left: 0.5rem; + line-height: 1.125rem; + + font-family: var(--font-family-monospace); + font-size: var(--font-size-monospace-normal); +} + +.Hook .Hook { + padding-left: 1rem; +} + +.Name { + color: var(--color-dim); + flex: 0 0 auto; + cursor: default; +} + +.PrimitiveHookName { + color: var(--color-text); + flex: 0 0 auto; + cursor: default; +} + +.Name:after { + color: var(--color-text); + content: ': '; + margin-right: 0.5rem; +} + +.PrimitiveHookNumber { + background-color: var(--color-primitive-hook-badge-background); + color: var(--color-primitive-hook-badge-text); + font-size: var(--font-size-monospace-small); + margin-right: 0.25rem; + border-radius: 0.125rem; + padding: 0.125rem 0.25rem; +} + +.HookName { + color: var(--color-component-name); +} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/HookChangeSummary.js b/packages/react-devtools-shared/src/devtools/views/Profiler/HookChangeSummary.js new file mode 100644 index 0000000000000..ed852c85c778a --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/HookChangeSummary.js @@ -0,0 +1,207 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 { + 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'; + +// $FlowFixMe: Flow doesn't know about Intl.ListFormat +const hookListFormatter = new Intl.ListFormat('en', { + style: 'long', + type: 'conjunction', +}); + +type HookProps = { + hook: HooksNode, + hookNames: Map | null, +}; + +const Hook: React.AbstractComponent = 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 ( +
    +
  • + {hook.id !== null && ( + + {String(hook.id + 1)} + + )} + + {hook.name} + {hookName && ({hookName})} + + {hook.subHooks?.map((subHook, index) => ( + + ))} +
  • +
+ ); +}); + +const shouldKeepHook = ( + hook: HooksNode, + hooksArray: Array, +): 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, +): 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, + state: $PropertyType, + displayMode?: 'detailed' | 'compact', +|}; + +const HookChangeSummary: React.AbstractComponent = memo( + ({hooks, fiberID, state, displayMode = 'detailed'}: Props) => { + const {parseHookNames, toggleParseHookNames, inspectedElement} = useContext( + InspectedElementContext, + ); + const store = useContext(StoreContext); + + const [parseHookNamesOptimistic, setParseHookNamesOptimistic] = + useState(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 No hooks changed; + } + + 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 ( + + {hookWord} {hookListFormatter.format(hookIds)} changed + + ); + } + + 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 ( +
+ {filteredHooks.length > 1 ? '• Hooks changed:' : '• Hook changed:'} + {(!parseHookNames || hookParsingFailed) && ( + + + + )} + {filteredHooks.map(hook => ( + + ))} +
+ ); + }, +); + +export default HookChangeSummary; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.js b/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.js index f1156a05aee14..a6dfeffb3e20e 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.js @@ -95,7 +95,7 @@ export default function HoveredFiberInfo({fiberData}: Props): React.Node {
{renderDurationInfo ||
Did not client render.
} - +
diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.js b/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.js index 52fc756a063d1..dffcd03350cda 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.js @@ -14,30 +14,17 @@ import {ProfilerContext} from './ProfilerContext'; import {StoreContext} from '../context'; import styles from './WhatChanged.css'; - -function hookIndicesToString(indices: Array): string { - // This is debatable but I think 1-based might ake for a nicer UX. - const numbers = indices.map(value => value + 1); - - switch (numbers.length) { - case 0: - return 'No hooks changed'; - case 1: - return `Hook ${numbers[0]} changed`; - case 2: - return `Hooks ${numbers[0]} and ${numbers[1]} changed`; - default: - return `Hooks ${numbers.slice(0, numbers.length - 1).join(', ')} and ${ - numbers[numbers.length - 1] - } changed`; - } -} +import HookChangeSummary from './HookChangeSummary'; type Props = { fiberID: number, + displayMode?: 'detailed' | 'compact', }; -export default function WhatChanged({fiberID}: Props): React.Node { +export default function WhatChanged({ + fiberID, + displayMode = 'detailed', +}: Props): React.Node { const {profilerStore} = useContext(StoreContext); const {rootID, selectedCommitIndex} = useContext(ProfilerContext); @@ -106,7 +93,12 @@ export default function WhatChanged({fiberID}: Props): React.Node { if (Array.isArray(hooks)) { changes.push(
- • {hookIndicesToString(hooks)} +
, ); } else { diff --git a/packages/react-devtools-shared/src/hookNamesCache.js b/packages/react-devtools-shared/src/hookNamesCache.js index 89377eebb18fd..e0d4b1075c219 100644 --- a/packages/react-devtools-shared/src/hookNamesCache.js +++ b/packages/react-devtools-shared/src/hookNamesCache.js @@ -72,6 +72,14 @@ export function hasAlreadyLoadedHookNames(element: Element): boolean { return record != null && record.status === Resolved; } +export function getAlreadyLoadedHookNames(element: Element): HookNames | null { + const record = map.get(element); + if (record != null && record.status === Resolved) { + return record.value; + } + return null; +} + export function loadHookNames( element: Element, hooksTree: HooksTree,