feat[devtools]: display Forget badge for the relevant components (#27709)

Adds `Forget` badge to all relevant components.

Changes:
- If component is compiled with Forget and using a built-in
`useMemoCache` hook, it will have a `Forget` badge next to its display
name in:
  - components tree
  - inspected element view
  - owners list
- Such badges are indexable, so Forget components can be searched using
search bar.

Fixes:
- Displaying the badges for owners list inside the inspected component
view

Implementation:
- React DevTools backend is responsible for identifying if component is
compiled with Forget, based on `fiber.updateQueue.memoCache`. It will
wrap component's display name with `Forget(...)` prefix before passing
operations to the frontend. On the frontend side, we will parse the
display name and strip Forget prefix, marking the corresponding element
by setting `compiledWithForget` field. Almost the same logic is
currently used for HOC display names.
This commit is contained in:
Ruslan Lesiutin
2023-11-23 18:37:21 +00:00
committed by GitHub
parent fbc9b68d61
commit 6c7b41da3d
29 changed files with 428 additions and 225 deletions

View File

@@ -2765,6 +2765,7 @@ describe('InspectedElement', () => {
expect(inspectedElement.owners).toMatchInlineSnapshot(`
[
{
"compiledWithForget": false,
"displayName": "Child",
"hocDisplayNames": null,
"id": 3,
@@ -2772,6 +2773,7 @@ describe('InspectedElement', () => {
"type": 5,
},
{
"compiledWithForget": false,
"displayName": "App",
"hocDisplayNames": null,
"id": 2,

View File

@@ -968,6 +968,7 @@ describe('ProfilingCache', () => {
"timestamp": 0,
"updaters": [
{
"compiledWithForget": false,
"displayName": "render()",
"hocDisplayNames": null,
"id": 1,
@@ -1010,6 +1011,7 @@ describe('ProfilingCache', () => {
"timestamp": 0,
"updaters": [
{
"compiledWithForget": false,
"displayName": "render()",
"hocDisplayNames": null,
"id": 1,

View File

@@ -424,7 +424,10 @@ export function getInternalReactConstants(version: string): {
}
// NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods
function getDisplayNameForFiber(fiber: Fiber): string | null {
function getDisplayNameForFiber(
fiber: Fiber,
shouldSkipForgetCheck: boolean = false,
): string | null {
const {elementType, type, tag} = fiber;
let resolvedType = type;
@@ -433,6 +436,18 @@ export function getInternalReactConstants(version: string): {
}
let resolvedContext: any = null;
// $FlowFixMe[incompatible-type] fiber.updateQueue is mixed
if (!shouldSkipForgetCheck && fiber.updateQueue?.memoCache != null) {
const displayNameWithoutForgetWrapper = getDisplayNameForFiber(
fiber,
true,
);
if (displayNameWithoutForgetWrapper == null) {
return null;
}
return `Forget(${displayNameWithoutForgetWrapper})`;
}
switch (tag) {
case CacheComponent:

View File

@@ -8,7 +8,7 @@
*/
import {hydrate, fillInPath} from 'react-devtools-shared/src/hydration';
import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
import {backendToFrontendSerializedElementMapper} from 'react-devtools-shared/src/utils';
import Store from 'react-devtools-shared/src/devtools/store';
import TimeoutError from 'react-devtools-shared/src/errors/TimeoutError';
import ElementPollingCancellationError from 'react-devtools-shared/src/errors/ElementPollingCancellationError';
@@ -266,17 +266,7 @@ export function convertInspectedElementBackendToFrontend(
owners:
owners === null
? null
: owners.map(owner => {
const [displayName, hocDisplayNames] = separateDisplayNameAndHOCs(
owner.displayName,
owner.type,
);
return {
...owner,
displayName,
hocDisplayNames,
};
}),
: owners.map(backendToFrontendSerializedElementMapper),
context: hydrateHelper(context),
hooks: hydrateHelper(hooks),
props: hydrateHelper(props),

View File

@@ -77,6 +77,7 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
'--color-error-border': 'hsl(0, 100%, 92%)',
'--color-error-text': '#ff0000',
'--color-expand-collapse-toggle': '#777d88',
'--color-forget-badge': '#2683E2',
'--color-link': '#0000ff',
'--color-modal-background': 'rgba(255, 255, 255, 0.75)',
'--color-bridge-version-npm-background': '#eff0f1',
@@ -221,6 +222,7 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
'--color-error-border': '#900',
'--color-error-text': '#f55',
'--color-expand-collapse-toggle': '#8f949d',
'--color-forget-badge': '#2683E2',
'--color-link': '#61dafb',
'--color-modal-background': 'rgba(0, 0, 0, 0.75)',
'--color-bridge-version-npm-background': 'rgba(0, 0, 0, 0.25)',

View File

@@ -25,9 +25,9 @@ import {ElementTypeRoot} from '../frontend/types';
import {
getSavedComponentFilters,
setSavedComponentFilters,
separateDisplayNameAndHOCs,
shallowDiffers,
utfDecodeStringWithRanges,
parseElementDisplayNameFromBackend,
} from '../utils';
import {localStorageGetItem, localStorageSetItem} from '../storage';
import {__DEBUG__} from '../constants';
@@ -1033,6 +1033,7 @@ export default class Store extends EventEmitter<{
parentID: 0,
type,
weight: 0,
compiledWithForget: false,
});
haveRootsChanged = true;
@@ -1071,8 +1072,11 @@ export default class Store extends EventEmitter<{
parentElement.children.push(id);
const [displayNameWithoutHOCs, hocDisplayNames] =
separateDisplayNameAndHOCs(displayName, type);
const {
formattedDisplayName: displayNameWithoutHOCs,
hocDisplayNames,
compiledWithForget,
} = parseElementDisplayNameFromBackend(displayName, type);
const element: Element = {
children: [],
@@ -1087,6 +1091,7 @@ export default class Store extends EventEmitter<{
parentID,
type,
weight: 1,
compiledWithForget,
};
this._idToElement.set(id, element);

View File

@@ -9,9 +9,3 @@
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-small);
}
.ExtraLabel {
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-small);
color: var(--color-component-badge-count);
}

View File

@@ -8,36 +8,14 @@
*/
import * as React from 'react';
import {Fragment} from 'react';
import styles from './Badge.css';
import type {ElementType} from 'react-devtools-shared/src/frontend/types';
import styles from './Badge.css';
type Props = {
className?: string,
hocDisplayNames: Array<string> | null,
type: ElementType,
children: React$Node,
};
export default function Badge({
className,
hocDisplayNames,
type,
children,
}: Props): React.Node {
if (hocDisplayNames === null || hocDisplayNames.length === 0) {
return null;
}
const totalBadgeCount = hocDisplayNames.length;
return (
<Fragment>
<div className={`${styles.Badge} ${className || ''}`}>{children}</div>
{totalBadgeCount > 1 && (
<div className={styles.ExtraLabel}>+{totalBadgeCount - 1}</div>
)}
</Fragment>
);
export default function Badge({className = '', children}: Props): React.Node {
return <div className={`${styles.Badge} ${className}`}>{children}</div>;
}

View File

@@ -65,7 +65,7 @@
color: var(--color-expand-collapse-toggle);
}
.Badge {
.BadgesBlock {
margin-left: 0.25rem;
}

View File

@@ -10,14 +10,14 @@
import * as React from 'react';
import {Fragment, useContext, useMemo, useState} from 'react';
import Store from 'react-devtools-shared/src/devtools/store';
import Badge from './Badge';
import ButtonIcon from '../ButtonIcon';
import {createRegExp} from '../utils';
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
import {SettingsContext} from '../Settings/SettingsContext';
import {StoreContext} from '../context';
import {useSubscription} from '../hooks';
import {logEvent} from 'react-devtools-shared/src/Logger';
import IndexableElementBadges from './IndexableElementBadges';
import IndexableDisplayName from './IndexableDisplayName';
import type {ItemData} from './Tree';
import type {Element as ElementType} from 'react-devtools-shared/src/frontend/types';
@@ -121,7 +121,7 @@ export default function Element({data, index, style}: Props): React.Node {
hocDisplayNames,
isStrictModeNonCompliant,
key,
type,
compiledWithForget,
} = element;
// Only show strict mode non-compliance badges for top level elements.
@@ -155,11 +155,11 @@ export default function Element({data, index, style}: Props): React.Node {
// We must use padding rather than margin/left because of the selected background color.
transform: `translateX(calc(${depth} * var(--indentation-size)))`,
}}>
{ownerID === null ? (
{ownerID === null && (
<ExpandCollapseToggle element={element} store={store} />
) : null}
)}
<DisplayName displayName={displayName} id={((id: any): number)} />
<IndexableDisplayName displayName={displayName} id={id} />
{key && (
<Fragment>
@@ -174,14 +174,12 @@ export default function Element({data, index, style}: Props): React.Node {
</Fragment>
)}
{hocDisplayNames !== null && hocDisplayNames.length > 0 ? (
<Badge
className={styles.Badge}
hocDisplayNames={hocDisplayNames}
type={type}>
<DisplayName displayName={hocDisplayNames[0]} id={id} />
</Badge>
) : null}
<IndexableElementBadges
hocDisplayNames={hocDisplayNames}
compiledWithForget={compiledWithForget}
elementID={id}
className={styles.BadgesBlock}
/>
{showInlineWarningsAndErrors && errorCount > 0 && (
<Icon
@@ -262,47 +260,3 @@ function ExpandCollapseToggle({element, store}: ExpandCollapseToggleProps) {
</div>
);
}
type DisplayNameProps = {
displayName: string | null,
id: number,
};
function DisplayName({displayName, id}: DisplayNameProps) {
const {searchIndex, searchResults, searchText} = useContext(TreeStateContext);
const isSearchResult = useMemo(() => {
return searchResults.includes(id);
}, [id, searchResults]);
const isCurrentResult =
searchIndex !== null && id === searchResults[searchIndex];
if (!isSearchResult || displayName === null) {
return displayName;
}
const match = createRegExp(searchText).exec(displayName);
if (match === null) {
return displayName;
}
const startIndex = match.index;
const stopIndex = startIndex + match[0].length;
const children = [];
if (startIndex > 0) {
children.push(<span key="begin">{displayName.slice(0, startIndex)}</span>);
}
children.push(
<mark
key="middle"
className={isCurrentResult ? styles.CurrentHighlight : styles.Highlight}>
{displayName.slice(startIndex, stopIndex)}
</mark>,
);
if (stopIndex < displayName.length) {
children.push(<span key="end">{displayName.slice(stopIndex)}</span>);
}
return children;
}

View File

@@ -0,0 +1,14 @@
.Root {
display: inline-flex;
align-items: center;
}
.Root *:not(:first-child) {
margin-left: 0.25rem;
}
.ExtraLabel {
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-small);
color: var(--color-component-badge-count);
}

View File

@@ -0,0 +1,48 @@
/**
* 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 Badge from './Badge';
import ForgetBadge from './ForgetBadge';
import styles from './ElementBadges.css';
type Props = {
hocDisplayNames: Array<string> | null,
compiledWithForget: boolean,
className?: string,
};
export default function ElementBadges({
compiledWithForget,
hocDisplayNames,
className = '',
}: Props): React.Node {
if (
!compiledWithForget &&
(hocDisplayNames == null || hocDisplayNames.length === 0)
) {
return null;
}
return (
<div className={`${styles.Root} ${className}`}>
{compiledWithForget && <ForgetBadge indexable={false} />}
{hocDisplayNames != null && hocDisplayNames.length > 0 && (
<Badge>{hocDisplayNames[0]}</Badge>
)}
{hocDisplayNames != null && hocDisplayNames.length > 1 && (
<div className={styles.ExtraLabel}>+{hocDisplayNames.length - 1}</div>
)}
</div>
);
}

View File

@@ -0,0 +1,3 @@
.Root {
background-color: var(--color-forget-badge);
}

View File

@@ -0,0 +1,43 @@
/**
* 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 Badge from './Badge';
import IndexableDisplayName from './IndexableDisplayName';
import styles from './ForgetBadge.css';
type CommonProps = {
className?: string,
};
type PropsForIndexable = CommonProps & {
indexable: true,
elementID: number,
};
type PropsForNonIndexable = CommonProps & {
indexable: false | void,
elementID?: number,
};
type Props = PropsForIndexable | PropsForNonIndexable;
export default function ForgetBadge(props: Props): React.Node {
const {className = ''} = props;
const innerView = props.indexable ? (
<IndexableDisplayName displayName="Forget" id={props.elementID} />
) : (
'Forget'
);
return <Badge className={`${styles.Root} ${className}`}>{innerView}</Badge>;
}

View File

@@ -1,16 +0,0 @@
.HocBadges {
padding: 0.125rem 0.25rem;
user-select: none;
}
.Badge {
display: inline-block;
background-color: var(--color-component-badge-background);
color: var(--color-text);
padding: 0.125rem 0.25rem;
line-height: normal;
border-radius: 0.125rem;
margin-right: 0.25rem;
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-small);
}

View File

@@ -1,35 +0,0 @@
/**
* 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 styles from './HocBadges.css';
import type {Element} from 'react-devtools-shared/src/frontend/types';
type Props = {
element: Element,
};
export default function HocBadges({element}: Props): React.Node {
const {hocDisplayNames} = ((element: any): Element);
if (hocDisplayNames === null) {
return null;
}
return (
<div className={styles.HocBadges}>
{hocDisplayNames.map(hocDisplayName => (
<div key={hocDisplayName} className={styles.Badge}>
{hocDisplayName}
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,63 @@
/**
* 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 {createRegExp} from '../utils';
import {TreeStateContext} from './TreeContext';
import styles from './Element.css';
const {useMemo, useContext} = React;
type Props = {
displayName: string | null,
id: number,
};
function IndexableDisplayName({displayName, id}: Props): React.Node {
const {searchIndex, searchResults, searchText} = useContext(TreeStateContext);
const isSearchResult = useMemo(() => {
return searchResults.includes(id);
}, [id, searchResults]);
const isCurrentResult =
searchIndex !== null && id === searchResults[searchIndex];
if (!isSearchResult || displayName === null) {
return displayName;
}
const match = createRegExp(searchText).exec(displayName);
if (match === null) {
return displayName;
}
const startIndex = match.index;
const stopIndex = startIndex + match[0].length;
const children = [];
if (startIndex > 0) {
children.push(<span key="begin">{displayName.slice(0, startIndex)}</span>);
}
children.push(
<mark
key="middle"
className={isCurrentResult ? styles.CurrentHighlight : styles.Highlight}>
{displayName.slice(startIndex, stopIndex)}
</mark>,
);
if (stopIndex < displayName.length) {
children.push(<span key="end">{displayName.slice(stopIndex)}</span>);
}
return children;
}
export default IndexableDisplayName;

View File

@@ -0,0 +1,14 @@
.Root {
display: inline-flex;
align-items: center;
}
.Root *:not(:first-child) {
margin-left: 0.25rem;
}
.ExtraLabel {
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-small);
color: var(--color-component-badge-count);
}

View File

@@ -0,0 +1,58 @@
/**
* 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 Badge from './Badge';
import ForgetBadge from './ForgetBadge';
import IndexableDisplayName from './IndexableDisplayName';
import styles from './IndexableElementBadges.css';
type Props = {
hocDisplayNames: Array<string> | null,
compiledWithForget: boolean,
elementID: number,
className?: string,
};
export default function IndexableElementBadges({
compiledWithForget,
hocDisplayNames,
elementID,
className = '',
}: Props): React.Node {
if (
!compiledWithForget &&
(hocDisplayNames == null || hocDisplayNames.length === 0)
) {
return null;
}
return (
<div className={`${styles.Root} ${className}`}>
{compiledWithForget && (
<ForgetBadge indexable={true} elementID={elementID} />
)}
{hocDisplayNames != null && hocDisplayNames.length > 0 && (
<Badge>
<IndexableDisplayName
displayName={hocDisplayNames[0]}
id={elementID}
/>
</Badge>
)}
{hocDisplayNames != null && hocDisplayNames.length > 1 && (
<div className={styles.ExtraLabel}>+{hocDisplayNames.length - 1}</div>
)}
</div>
);
}

View File

@@ -0,0 +1,9 @@
.Root {
padding: 0.25rem;
user-select: none;
display: inline-flex;
}
.Root *:not(:first-child) {
margin-left: 0.25rem;
}

View File

@@ -0,0 +1,36 @@
/**
* 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 type {Element} from 'react-devtools-shared/src/frontend/types';
import * as React from 'react';
import Badge from './Badge';
import ForgetBadge from './ForgetBadge';
import styles from './InspectedElementBadges.css';
type Props = {
element: Element,
};
export default function InspectedElementBadges({element}: Props): React.Node {
const {hocDisplayNames, compiledWithForget} = element;
return (
<div className={styles.Root}>
{compiledWithForget && <ForgetBadge indexable={false} />}
{hocDisplayNames !== null &&
hocDisplayNames.map(hocDisplayName => (
<Badge key={hocDisplayName}>{hocDisplayName}</Badge>
))}
</div>
);
}

View File

@@ -17,7 +17,7 @@ import ContextMenuItem from '../../ContextMenu/ContextMenuItem';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import Icon from '../Icon';
import HocBadges from './HocBadges';
import InspectedElementBadges from './InspectedElementBadges';
import InspectedElementContextTree from './InspectedElementContextTree';
import InspectedElementErrorsAndWarningsTree from './InspectedElementErrorsAndWarningsTree';
import InspectedElementHooksTree from './InspectedElementHooksTree';
@@ -26,7 +26,7 @@ import InspectedElementStateTree from './InspectedElementStateTree';
import InspectedElementStyleXPlugin from './InspectedElementStyleXPlugin';
import InspectedElementSuspenseToggle from './InspectedElementSuspenseToggle';
import NativeStyleEditor from './NativeStyleEditor';
import Badge from './Badge';
import ElementBadges from './ElementBadges';
import {useHighlightNativeElement} from '../hooks';
import {
copyInspectedElementPath as copyInspectedElementPathAPI,
@@ -41,12 +41,8 @@ import type {ContextMenuContextType} from '../context';
import type {
Element,
InspectedElement,
SerializedElement,
} from 'react-devtools-shared/src/frontend/types';
import type {
ElementType,
HookNames,
} from 'react-devtools-shared/src/frontend/types';
import type {HookNames} from 'react-devtools-shared/src/frontend/types';
import type {ToggleParseHookNames} from './InspectedElementContext';
export type CopyPath = (path: Array<string | number>) => void;
@@ -90,7 +86,7 @@ export default function InspectedElementView({
return (
<Fragment>
<div className={styles.InspectedElement}>
<HocBadges element={element} />
<InspectedElementBadges element={element} />
<InspectedElementPropsTree
bridge={bridge}
@@ -152,17 +148,20 @@ export default function InspectedElementView({
className={styles.Owners}
data-testname="InspectedElementView-Owners">
<div className={styles.OwnersHeader}>rendered by</div>
{showOwnersList &&
((owners: any): Array<SerializedElement>).map(owner => (
owners?.map(owner => (
<OwnerView
key={owner.id}
displayName={owner.displayName || 'Anonymous'}
hocDisplayNames={owner.hocDisplayNames}
compiledWithForget={owner.compiledWithForget}
id={owner.id}
isInStore={store.containsElement(owner.id)}
type={owner.type}
/>
))}
{rootType !== null && (
<div className={styles.OwnersMetaField}>{rootType}</div>
)}
@@ -286,17 +285,17 @@ function Source({fileName, lineNumber}: SourceProps) {
type OwnerViewProps = {
displayName: string,
hocDisplayNames: Array<string> | null,
compiledWithForget: boolean,
id: number,
isInStore: boolean,
type: ElementType,
};
function OwnerView({
displayName,
hocDisplayNames,
compiledWithForget,
id,
isInStore,
type,
}: OwnerViewProps) {
const dispatch = useContext(TreeDispatcherContext);
const {highlightNativeElement, clearHighlightNativeElement} =
@@ -313,25 +312,25 @@ function OwnerView({
});
}, [dispatch, id]);
const onMouseEnter = () => highlightNativeElement(id);
const onMouseLeave = clearHighlightNativeElement;
return (
<Button
key={id}
className={styles.OwnerButton}
disabled={!isInStore}
onClick={handleClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}>
onMouseEnter={() => highlightNativeElement(id)}
onMouseLeave={clearHighlightNativeElement}>
<span className={styles.OwnerContent}>
<span
className={`${styles.Owner} ${isInStore ? '' : styles.NotInStore}`}
title={displayName}>
{displayName}
</span>
<Badge hocDisplayNames={hocDisplayNames} type={type} />
<ElementBadges
hocDisplayNames={hocDisplayNames}
compiledWithForget={compiledWithForget}
/>
</span>
</Button>
);

View File

@@ -14,7 +14,7 @@ import {createContext, useCallback, useContext, useEffect} from 'react';
import {createResource} from '../../cache';
import {BridgeContext, StoreContext} from '../context';
import {TreeStateContext} from './TreeContext';
import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
import {backendToFrontendSerializedElementMapper} from 'react-devtools-shared/src/utils';
import type {OwnersList} from 'react-devtools-shared/src/backend/types';
import type {
@@ -100,16 +100,7 @@ function OwnersListContextController({children}: Props): React.Node {
request.resolveFn(
ownersList.owners === null
? null
: ownersList.owners.map(owner => {
const [displayNameWithoutHOCs, hocDisplayNames] =
separateDisplayNameAndHOCs(owner.displayName, owner.type);
return {
...owner,
displayName: displayNameWithoutHOCs,
hocDisplayNames,
};
}),
: ownersList.owners.map(backendToFrontendSerializedElementMapper),
);
}
}

View File

@@ -99,6 +99,6 @@
color: var(--color-dimmest);
}
.Badge {
.BadgesBlock {
margin-left: 0.25rem;
}

View File

@@ -19,7 +19,7 @@ import {
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import Toggle from '../Toggle';
import Badge from './Badge';
import ElementBadges from './ElementBadges';
import {OwnersListContext} from './OwnersListContext';
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
import {useIsOverflowing} from '../hooks';
@@ -204,11 +204,7 @@ type ElementsDropdownProps = {
selectOwner: SelectOwner,
...
};
function ElementsDropdown({
owners,
selectedIndex,
selectOwner,
}: ElementsDropdownProps) {
function ElementsDropdown({owners, selectOwner}: ElementsDropdownProps) {
const store = useContext(StoreContext);
const menuItems = [];
@@ -222,10 +218,10 @@ function ElementsDropdown({
onSelect={() => (isInStore ? selectOwner(owner) : null)}>
{owner.displayName}
<Badge
className={styles.Badge}
<ElementBadges
hocDisplayNames={owner.hocDisplayNames}
type={owner.type}
compiledWithForget={owner.compiledWithForget}
className={styles.BadgesBlock}
/>
</MenuItem>,
);
@@ -254,7 +250,7 @@ type ElementViewProps = {
function ElementView({isSelected, owner, selectOwner}: ElementViewProps) {
const store = useContext(StoreContext);
const {displayName, hocDisplayNames, type} = owner;
const {displayName, hocDisplayNames, compiledWithForget} = owner;
const isInStore = store.containsElement(owner.id);
const handleChange = useCallback(() => {
@@ -270,10 +266,10 @@ function ElementView({isSelected, owner, selectOwner}: ElementViewProps) {
onChange={handleChange}>
{displayName}
<Badge
className={styles.Badge}
<ElementBadges
hocDisplayNames={hocDisplayNames}
type={type}
compiledWithForget={compiledWithForget}
className={styles.BadgesBlock}
/>
</Toggle>
);

View File

@@ -993,10 +993,13 @@ function recursivelySearchTree(
regExp: RegExp,
searchResults: Array<number>,
): void {
const {children, displayName, hocDisplayNames} = ((store.getElementByID(
elementID,
): any): Element);
const element = store.getElementByID(elementID);
if (element == null) {
return;
}
const {children, displayName, hocDisplayNames, compiledWithForget} = element;
if (displayName != null && regExp.test(displayName) === true) {
searchResults.push(elementID);
} else if (
@@ -1005,6 +1008,8 @@ function recursivelySearchTree(
hocDisplayNames.some(name => regExp.test(name)) === true
) {
searchResults.push(elementID);
} else if (compiledWithForget && regExp.test('Forget')) {
searchResults.push(elementID);
}
children.forEach(childID =>

View File

@@ -8,7 +8,7 @@
*/
import {PROFILER_EXPORT_VERSION} from 'react-devtools-shared/src/constants';
import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
import {backendToFrontendSerializedElementMapper} from 'react-devtools-shared/src/utils';
import type {ProfilingDataBackend} from 'react-devtools-shared/src/backend/types';
import type {
@@ -106,20 +106,9 @@ export function prepareProfilingDataFrontendFromBackendAndStore(
timestamp: commitDataBackend.timestamp,
updaters:
commitDataBackend.updaters !== null
? commitDataBackend.updaters.map(serializedElement => {
const [
serializedElementDisplayName,
serializedElementHocDisplayNames,
] = separateDisplayNameAndHOCs(
serializedElement.displayName,
serializedElement.type,
);
return {
...serializedElement,
displayName: serializedElementDisplayName,
hocDisplayNames: serializedElementHocDisplayNames,
};
})
? commitDataBackend.updaters.map(
backendToFrontendSerializedElementMapper,
)
: null,
}),
);

View File

@@ -151,6 +151,10 @@ export type Element = {
// This element is not in a StrictMode compliant subtree.
// Only true for React versions supporting StrictMode.
isStrictModeNonCompliant: boolean,
// If component is compiled with Forget, the backend will send its name as Forget(...)
// Later, on the frontend side, we will strip HOC names and Forget prefix.
compiledWithForget: boolean,
};
export type SerializedElement = {
@@ -158,6 +162,7 @@ export type SerializedElement = {
id: number,
key: number | string | null,
hocDisplayNames: Array<string> | null,
compiledWithForget: boolean,
type: ElementType,
};

View File

@@ -60,8 +60,10 @@ import type {
ComponentFilter,
ElementType,
BrowserTheme,
} from './frontend/types';
import type {LRUCache} from 'react-devtools-shared/src/frontend/types';
SerializedElement as SerializedElementFrontend,
LRUCache,
} from 'react-devtools-shared/src/frontend/types';
import type {SerializedElement as SerializedElementBackend} from 'react-devtools-shared/src/backend/types';
// $FlowFixMe[method-unbinding]
const hasOwnProperty = Object.prototype.hasOwnProperty;
@@ -415,16 +417,35 @@ export function getOpenInEditorURL(): string {
return getDefaultOpenInEditorURL();
}
export function separateDisplayNameAndHOCs(
type ParseElementDisplayNameFromBackendReturn = {
formattedDisplayName: string | null,
hocDisplayNames: Array<string> | null,
compiledWithForget: boolean,
};
export function parseElementDisplayNameFromBackend(
displayName: string | null,
type: ElementType,
): [string | null, Array<string> | null] {
): ParseElementDisplayNameFromBackendReturn {
if (displayName === null) {
return [null, null];
return {
formattedDisplayName: null,
hocDisplayNames: null,
compiledWithForget: false,
};
}
if (displayName.startsWith('Forget(')) {
const displayNameWithoutForgetWrapper = displayName.slice(
7,
displayName.length - 1,
);
const {formattedDisplayName, hocDisplayNames} =
parseElementDisplayNameFromBackend(displayNameWithoutForgetWrapper, type);
return {formattedDisplayName, hocDisplayNames, compiledWithForget: true};
}
let hocDisplayNames = null;
switch (type) {
case ElementTypeClass:
case ElementTypeForwardRef:
@@ -442,7 +463,11 @@ export function separateDisplayNameAndHOCs(
break;
}
return [displayName, hocDisplayNames];
return {
formattedDisplayName: displayName,
hocDisplayNames,
compiledWithForget: false,
};
}
// Pulled from react-compat
@@ -897,3 +922,17 @@ export const isPlainObject = (object: Object): boolean => {
const objectParentPrototype = Object.getPrototypeOf(objectPrototype);
return !objectParentPrototype;
};
export function backendToFrontendSerializedElementMapper(
element: SerializedElementBackend,
): SerializedElementFrontend {
const {formattedDisplayName, hocDisplayNames, compiledWithForget} =
parseElementDisplayNameFromBackend(element.displayName, element.type);
return {
...element,
displayName: formattedDisplayName,
hocDisplayNames,
compiledWithForget,
};
}