Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -162,9 +161,7 @@ function Components(_: {}) {
<div className={styles.InspectedElementWrapper}>
<NativeStyleContextController>
<InspectedElementErrorBoundary>
<InspectedElementContextController>
<InspectedElement />
</InspectedElementContextController>
<InspectedElement />
</InspectedElementErrorBoundary>
</NativeStyleContextController>
</div>
Expand Down
73 changes: 39 additions & 34 deletions packages/react-devtools-shared/src/devtools/views/DevTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -276,43 +277,47 @@ export default function DevTools({
<TreeContextController>
<ProfilerContextController>
<TimelineContextController>
<ThemeProvider>
<div
className={styles.DevTools}
ref={devToolsRef}
data-react-devtools-portal-root={true}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={selectTab}
tabs={tabs}
type="navigation"
<InspectedElementContextController>
<ThemeProvider>
<div
className={styles.DevTools}
ref={devToolsRef}
data-react-devtools-portal-root={true}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={selectTab}
tabs={tabs}
type="navigation"
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}>
<Components
portalContainer={
componentsPortalContainer
}
/>
</div>
<div
className={styles.TabContent}
hidden={tab !== 'profiler'}>
<Profiler
portalContainer={profilerPortalContainer}
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}>
<Components
portalContainer={componentsPortalContainer}
/>
</div>
<div
className={styles.TabContent}
hidden={tab !== 'profiler'}>
<Profiler
portalContainer={profilerPortalContainer}
/>
</div>
</div>
</ThemeProvider>
</ThemeProvider>
</InspectedElementContextController>
</TimelineContextController>
</ProfilerContextController>
</TreeContextController>
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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<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;
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export default function HoveredFiberInfo({fiberData}: Props): React.Node {
<div className={styles.Content}>
{renderDurationInfo || <div>Did not client render.</div>}

<WhatChanged fiberID={id} />
<WhatChanged fiberID={id} displayMode="compact" />
</div>
</div>
</Fragment>
Expand Down
Loading
Loading