@samitouri / QOS-React-1 / commits / 6c7b41da3d

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.

Ruslan Lesiutin committed Nov 23, 2023 at 18:37 UTC 6c7b41da3de12be2d95c60181b3fe896f824f13a
29 files changed +428 -225
packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
+2
@@ -2765,6 +2765,7 @@ describe('InspectedElement', () => {
2765 expect(inspectedElement.owners).toMatchInlineSnapshot(`
2766 [
2767 {
2768 + "compiledWithForget": false,
2769 "displayName": "Child",
2770 "hocDisplayNames": null,
2771 "id": 3,
@@ -2772,6 +2773,7 @@ describe('InspectedElement', () => {
2773 "type": 5,
2774 },
2775 {
2776 + "compiledWithForget": false,
2777 "displayName": "App",
2778 "hocDisplayNames": null,
2779 "id": 2,
packages/react-devtools-shared/src/__tests__/profilingCache-test.js
+2
@@ -968,6 +968,7 @@ describe('ProfilingCache', () => {
968 "timestamp": 0,
969 "updaters": [
970 {
971 + "compiledWithForget": false,
972 "displayName": "render()",
973 "hocDisplayNames": null,
974 "id": 1,
@@ -1010,6 +1011,7 @@ describe('ProfilingCache', () => {
1011 "timestamp": 0,
1012 "updaters": [
1013 {
1014 + "compiledWithForget": false,
1015 "displayName": "render()",
1016 "hocDisplayNames": null,
1017 "id": 1,
packages/react-devtools-shared/src/backend/renderer.js
+16 -1
@@ -424,7 +424,10 @@ export function getInternalReactConstants(version: string): {
424 }
425
426 // NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods
427 - function getDisplayNameForFiber(fiber: Fiber): string | null {
427 + function getDisplayNameForFiber(
428 + fiber: Fiber,
429 + shouldSkipForgetCheck: boolean = false,
430 + ): string | null {
431 const {elementType, type, tag} = fiber;
432
433 let resolvedType = type;
@@ -433,6 +436,18 @@ export function getInternalReactConstants(version: string): {
436 }
437
438 let resolvedContext: any = null;
439 + // $FlowFixMe[incompatible-type] fiber.updateQueue is mixed
440 + if (!shouldSkipForgetCheck && fiber.updateQueue?.memoCache != null) {
441 + const displayNameWithoutForgetWrapper = getDisplayNameForFiber(
442 + fiber,
443 + true,
444 + );
445 + if (displayNameWithoutForgetWrapper == null) {
446 + return null;
447 + }
448 +
449 + return `Forget(${displayNameWithoutForgetWrapper})`;
450 + }
451
452 switch (tag) {
453 case CacheComponent:
packages/react-devtools-shared/src/backendAPI.js
+2 -12
@@ -8,7 +8,7 @@
8 */
9
10 import {hydrate, fillInPath} from 'react-devtools-shared/src/hydration';
11 -import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
11 +import {backendToFrontendSerializedElementMapper} from 'react-devtools-shared/src/utils';
12 import Store from 'react-devtools-shared/src/devtools/store';
13 import TimeoutError from 'react-devtools-shared/src/errors/TimeoutError';
14 import ElementPollingCancellationError from 'react-devtools-shared/src/errors/ElementPollingCancellationError';
@@ -266,17 +266,7 @@ export function convertInspectedElementBackendToFrontend(
266 owners:
267 owners === null
268 ? null
269 - : owners.map(owner => {
270 - const [displayName, hocDisplayNames] = separateDisplayNameAndHOCs(
271 - owner.displayName,
272 - owner.type,
273 - );
274 - return {
275 - ...owner,
276 - displayName,
277 - hocDisplayNames,
278 - };
279 - }),
269 + : owners.map(backendToFrontendSerializedElementMapper),
270 context: hydrateHelper(context),
271 hooks: hydrateHelper(hooks),
272 props: hydrateHelper(props),
packages/react-devtools-shared/src/devtools/constants.js
+2
@@ -77,6 +77,7 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
77 '--color-error-border': 'hsl(0, 100%, 92%)',
78 '--color-error-text': '#ff0000',
79 '--color-expand-collapse-toggle': '#777d88',
80 + '--color-forget-badge': '#2683E2',
81 '--color-link': '#0000ff',
82 '--color-modal-background': 'rgba(255, 255, 255, 0.75)',
83 '--color-bridge-version-npm-background': '#eff0f1',
@@ -221,6 +222,7 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
222 '--color-error-border': '#900',
223 '--color-error-text': '#f55',
224 '--color-expand-collapse-toggle': '#8f949d',
225 + '--color-forget-badge': '#2683E2',
226 '--color-link': '#61dafb',
227 '--color-modal-background': 'rgba(0, 0, 0, 0.75)',
228 '--color-bridge-version-npm-background': 'rgba(0, 0, 0, 0.25)',
packages/react-devtools-shared/src/devtools/store.js
+8 -3
@@ -25,9 +25,9 @@ import {ElementTypeRoot} from '../frontend/types';
25 import {
26 getSavedComponentFilters,
27 setSavedComponentFilters,
28 - separateDisplayNameAndHOCs,
28 shallowDiffers,
29 utfDecodeStringWithRanges,
30 + parseElementDisplayNameFromBackend,
31 } from '../utils';
32 import {localStorageGetItem, localStorageSetItem} from '../storage';
33 import {__DEBUG__} from '../constants';
@@ -1033,6 +1033,7 @@ export default class Store extends EventEmitter<{
1033 parentID: 0,
1034 type,
1035 weight: 0,
1036 + compiledWithForget: false,
1037 });
1038
1039 haveRootsChanged = true;
@@ -1071,8 +1072,11 @@ export default class Store extends EventEmitter<{
1072
1073 parentElement.children.push(id);
1074
1074 - const [displayNameWithoutHOCs, hocDisplayNames] =
1075 - separateDisplayNameAndHOCs(displayName, type);
1075 + const {
1076 + formattedDisplayName: displayNameWithoutHOCs,
1077 + hocDisplayNames,
1078 + compiledWithForget,
1079 + } = parseElementDisplayNameFromBackend(displayName, type);
1080
1081 const element: Element = {
1082 children: [],
@@ -1087,6 +1091,7 @@ export default class Store extends EventEmitter<{
1091 parentID,
1092 type,
1093 weight: 1,
1094 + compiledWithForget,
1095 };
1096
1097 this._idToElement.set(id, element);
packages/react-devtools-shared/src/devtools/views/Components/Badge.css
-6
@@ -9,9 +9,3 @@
9 font-family: var(--font-family-monospace);
10 font-size: var(--font-size-monospace-small);
11 }
12 -
13 -.ExtraLabel {
14 - font-family: var(--font-family-monospace);
15 - font-size: var(--font-size-monospace-small);
16 - color: var(--color-component-badge-count);
17 -}
packages/react-devtools-shared/src/devtools/views/Components/Badge.js
+3 -25
@@ -8,36 +8,14 @@
8 */
9
10 import * as React from 'react';
11 -import {Fragment} from 'react';
12 -import styles from './Badge.css';
11
14 -import type {ElementType} from 'react-devtools-shared/src/frontend/types';
12 +import styles from './Badge.css';
13
14 type Props = {
15 className?: string,
18 - hocDisplayNames: Array<string> | null,
19 - type: ElementType,
16 children: React$Node,
17 };
18
23 -export default function Badge({
24 - className,
25 - hocDisplayNames,
26 - type,
27 - children,
28 -}: Props): React.Node {
29 - if (hocDisplayNames === null || hocDisplayNames.length === 0) {
30 - return null;
31 - }
32 -
33 - const totalBadgeCount = hocDisplayNames.length;
34 -
35 - return (
36 - <Fragment>
37 - <div className={`${styles.Badge} ${className || ''}`}>{children}</div>
38 - {totalBadgeCount > 1 && (
39 - <div className={styles.ExtraLabel}>+{totalBadgeCount - 1}</div>
40 - )}
41 - </Fragment>
42 - );
19 +export default function Badge({className = '', children}: Props): React.Node {
20 + return <div className={`${styles.Badge} ${className}`}>{children}</div>;
21 }
packages/react-devtools-shared/src/devtools/views/Components/Element.css
+1 -1
@@ -65,7 +65,7 @@
65 color: var(--color-expand-collapse-toggle);
66 }
67
68 -.Badge {
68 +.BadgesBlock {
69 margin-left: 0.25rem;
70 }
71
packages/react-devtools-shared/src/devtools/views/Components/Element.js
+12 -58
@@ -10,14 +10,14 @@
10 import * as React from 'react';
11 import {Fragment, useContext, useMemo, useState} from 'react';
12 import Store from 'react-devtools-shared/src/devtools/store';
13 -import Badge from './Badge';
13 import ButtonIcon from '../ButtonIcon';
15 -import {createRegExp} from '../utils';
14 import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
15 import {SettingsContext} from '../Settings/SettingsContext';
16 import {StoreContext} from '../context';
17 import {useSubscription} from '../hooks';
18 import {logEvent} from 'react-devtools-shared/src/Logger';
19 +import IndexableElementBadges from './IndexableElementBadges';
20 +import IndexableDisplayName from './IndexableDisplayName';
21
22 import type {ItemData} from './Tree';
23 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 {
121 hocDisplayNames,
122 isStrictModeNonCompliant,
123 key,
124 - type,
124 + compiledWithForget,
125 } = element;
126
127 // 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 {
155 // We must use padding rather than margin/left because of the selected background color.
156 transform: `translateX(calc(${depth} * var(--indentation-size)))`,
157 }}>
158 - {ownerID === null ? (
158 + {ownerID === null && (
159 <ExpandCollapseToggle element={element} store={store} />
160 - ) : null}
160 + )}
161
162 - <DisplayName displayName={displayName} id={((id: any): number)} />
162 + <IndexableDisplayName displayName={displayName} id={id} />
163
164 {key && (
165 <Fragment>
@@ -174,14 +174,12 @@ export default function Element({data, index, style}: Props): React.Node {
174 </Fragment>
175 )}
176
177 - {hocDisplayNames !== null && hocDisplayNames.length > 0 ? (
178 - <Badge
179 - className={styles.Badge}
180 - hocDisplayNames={hocDisplayNames}
181 - type={type}>
182 - <DisplayName displayName={hocDisplayNames[0]} id={id} />
183 - </Badge>
184 - ) : null}
177 + <IndexableElementBadges
178 + hocDisplayNames={hocDisplayNames}
179 + compiledWithForget={compiledWithForget}
180 + elementID={id}
181 + className={styles.BadgesBlock}
182 + />
183
184 {showInlineWarningsAndErrors && errorCount > 0 && (
185 <Icon
@@ -262,47 +260,3 @@ function ExpandCollapseToggle({element, store}: ExpandCollapseToggleProps) {
260 </div>
261 );
262 }
265 -
266 -type DisplayNameProps = {
267 - displayName: string | null,
268 - id: number,
269 -};
270 -
271 -function DisplayName({displayName, id}: DisplayNameProps) {
272 - const {searchIndex, searchResults, searchText} = useContext(TreeStateContext);
273 - const isSearchResult = useMemo(() => {
274 - return searchResults.includes(id);
275 - }, [id, searchResults]);
276 - const isCurrentResult =
277 - searchIndex !== null && id === searchResults[searchIndex];
278 -
279 - if (!isSearchResult || displayName === null) {
280 - return displayName;
281 - }
282 -
283 - const match = createRegExp(searchText).exec(displayName);
284 -
285 - if (match === null) {
286 - return displayName;
287 - }
288 -
289 - const startIndex = match.index;
290 - const stopIndex = startIndex + match[0].length;
291 -
292 - const children = [];
293 - if (startIndex > 0) {
294 - children.push(<span key="begin">{displayName.slice(0, startIndex)}</span>);
295 - }
296 - children.push(
297 - <mark
298 - key="middle"
299 - className={isCurrentResult ? styles.CurrentHighlight : styles.Highlight}>
300 - {displayName.slice(startIndex, stopIndex)}
301 - </mark>,
302 - );
303 - if (stopIndex < displayName.length) {
304 - children.push(<span key="end">{displayName.slice(stopIndex)}</span>);
305 - }
306 -
307 - return children;
308 -}
packages/react-devtools-shared/src/devtools/views/Components/ElementBadges.css new
+14
@@ -0,0 +1,14 @@
1 +.Root {
2 + display: inline-flex;
3 + align-items: center;
4 +}
5 +
6 +.Root *:not(:first-child) {
7 + margin-left: 0.25rem;
8 +}
9 +
10 +.ExtraLabel {
11 + font-family: var(--font-family-monospace);
12 + font-size: var(--font-size-monospace-small);
13 + color: var(--color-component-badge-count);
14 +}
packages/react-devtools-shared/src/devtools/views/Components/ElementBadges.js new
+48
@@ -0,0 +1,48 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import * as React from 'react';
11 +
12 +import Badge from './Badge';
13 +import ForgetBadge from './ForgetBadge';
14 +
15 +import styles from './ElementBadges.css';
16 +
17 +type Props = {
18 + hocDisplayNames: Array<string> | null,
19 + compiledWithForget: boolean,
20 + className?: string,
21 +};
22 +
23 +export default function ElementBadges({
24 + compiledWithForget,
25 + hocDisplayNames,
26 + className = '',
27 +}: Props): React.Node {
28 + if (
29 + !compiledWithForget &&
30 + (hocDisplayNames == null || hocDisplayNames.length === 0)
31 + ) {
32 + return null;
33 + }
34 +
35 + return (
36 + <div className={`${styles.Root} ${className}`}>
37 + {compiledWithForget && <ForgetBadge indexable={false} />}
38 +
39 + {hocDisplayNames != null && hocDisplayNames.length > 0 && (
40 + <Badge>{hocDisplayNames[0]}</Badge>
41 + )}
42 +
43 + {hocDisplayNames != null && hocDisplayNames.length > 1 && (
44 + <div className={styles.ExtraLabel}>+{hocDisplayNames.length - 1}</div>
45 + )}
46 + </div>
47 + );
48 +}
packages/react-devtools-shared/src/devtools/views/Components/ForgetBadge.css new
+3
@@ -0,0 +1,3 @@
1 +.Root {
2 + background-color: var(--color-forget-badge);
3 +}
packages/react-devtools-shared/src/devtools/views/Components/ForgetBadge.js new
+43
@@ -0,0 +1,43 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import * as React from 'react';
11 +
12 +import Badge from './Badge';
13 +import IndexableDisplayName from './IndexableDisplayName';
14 +
15 +import styles from './ForgetBadge.css';
16 +
17 +type CommonProps = {
18 + className?: string,
19 +};
20 +
21 +type PropsForIndexable = CommonProps & {
22 + indexable: true,
23 + elementID: number,
24 +};
25 +
26 +type PropsForNonIndexable = CommonProps & {
27 + indexable: false | void,
28 + elementID?: number,
29 +};
30 +
31 +type Props = PropsForIndexable | PropsForNonIndexable;
32 +
33 +export default function ForgetBadge(props: Props): React.Node {
34 + const {className = ''} = props;
35 +
36 + const innerView = props.indexable ? (
37 + <IndexableDisplayName displayName="Forget" id={props.elementID} />
38 + ) : (
39 + 'Forget'
40 + );
41 +
42 + return <Badge className={`${styles.Root} ${className}`}>{innerView}</Badge>;
43 +}
packages/react-devtools-shared/src/devtools/views/Components/HocBadges.css deleted
-16
@@ -1,16 +0,0 @@
1 -.HocBadges {
2 - padding: 0.125rem 0.25rem;
3 - user-select: none;
4 -}
5 -
6 -.Badge {
7 - display: inline-block;
8 - background-color: var(--color-component-badge-background);
9 - color: var(--color-text);
10 - padding: 0.125rem 0.25rem;
11 - line-height: normal;
12 - border-radius: 0.125rem;
13 - margin-right: 0.25rem;
14 - font-family: var(--font-family-monospace);
15 - font-size: var(--font-size-monospace-small);
16 -}
packages/react-devtools-shared/src/devtools/views/Components/HocBadges.js deleted
-35
@@ -1,35 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @flow
8 - */
9 -
10 -import * as React from 'react';
11 -import styles from './HocBadges.css';
12 -
13 -import type {Element} from 'react-devtools-shared/src/frontend/types';
14 -
15 -type Props = {
16 - element: Element,
17 -};
18 -
19 -export default function HocBadges({element}: Props): React.Node {
20 - const {hocDisplayNames} = ((element: any): Element);
21 -
22 - if (hocDisplayNames === null) {
23 - return null;
24 - }
25 -
26 - return (
27 - <div className={styles.HocBadges}>
28 - {hocDisplayNames.map(hocDisplayName => (
29 - <div key={hocDisplayName} className={styles.Badge}>
30 - {hocDisplayName}
31 - </div>
32 - ))}
33 - </div>
34 - );
35 -}
packages/react-devtools-shared/src/devtools/views/Components/IndexableDisplayName.js new
+63
@@ -0,0 +1,63 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import * as React from 'react';
11 +
12 +import {createRegExp} from '../utils';
13 +
14 +import {TreeStateContext} from './TreeContext';
15 +import styles from './Element.css';
16 +
17 +const {useMemo, useContext} = React;
18 +
19 +type Props = {
20 + displayName: string | null,
21 + id: number,
22 +};
23 +
24 +function IndexableDisplayName({displayName, id}: Props): React.Node {
25 + const {searchIndex, searchResults, searchText} = useContext(TreeStateContext);
26 + const isSearchResult = useMemo(() => {
27 + return searchResults.includes(id);
28 + }, [id, searchResults]);
29 + const isCurrentResult =
30 + searchIndex !== null && id === searchResults[searchIndex];
31 +
32 + if (!isSearchResult || displayName === null) {
33 + return displayName;
34 + }
35 +
36 + const match = createRegExp(searchText).exec(displayName);
37 +
38 + if (match === null) {
39 + return displayName;
40 + }
41 +
42 + const startIndex = match.index;
43 + const stopIndex = startIndex + match[0].length;
44 +
45 + const children = [];
46 + if (startIndex > 0) {
47 + children.push(<span key="begin">{displayName.slice(0, startIndex)}</span>);
48 + }
49 + children.push(
50 + <mark
51 + key="middle"
52 + className={isCurrentResult ? styles.CurrentHighlight : styles.Highlight}>
53 + {displayName.slice(startIndex, stopIndex)}
54 + </mark>,
55 + );
56 + if (stopIndex < displayName.length) {
57 + children.push(<span key="end">{displayName.slice(stopIndex)}</span>);
58 + }
59 +
60 + return children;
61 +}
62 +
63 +export default IndexableDisplayName;
packages/react-devtools-shared/src/devtools/views/Components/IndexableElementBadges.css new
+14
@@ -0,0 +1,14 @@
1 +.Root {
2 + display: inline-flex;
3 + align-items: center;
4 +}
5 +
6 +.Root *:not(:first-child) {
7 + margin-left: 0.25rem;
8 +}
9 +
10 +.ExtraLabel {
11 + font-family: var(--font-family-monospace);
12 + font-size: var(--font-size-monospace-small);
13 + color: var(--color-component-badge-count);
14 +}
packages/react-devtools-shared/src/devtools/views/Components/IndexableElementBadges.js new
+58
@@ -0,0 +1,58 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import * as React from 'react';
11 +
12 +import Badge from './Badge';
13 +import ForgetBadge from './ForgetBadge';
14 +import IndexableDisplayName from './IndexableDisplayName';
15 +
16 +import styles from './IndexableElementBadges.css';
17 +
18 +type Props = {
19 + hocDisplayNames: Array<string> | null,
20 + compiledWithForget: boolean,
21 + elementID: number,
22 + className?: string,
23 +};
24 +
25 +export default function IndexableElementBadges({
26 + compiledWithForget,
27 + hocDisplayNames,
28 + elementID,
29 + className = '',
30 +}: Props): React.Node {
31 + if (
32 + !compiledWithForget &&
33 + (hocDisplayNames == null || hocDisplayNames.length === 0)
34 + ) {
35 + return null;
36 + }
37 +
38 + return (
39 + <div className={`${styles.Root} ${className}`}>
40 + {compiledWithForget && (
41 + <ForgetBadge indexable={true} elementID={elementID} />
42 + )}
43 +
44 + {hocDisplayNames != null && hocDisplayNames.length > 0 && (
45 + <Badge>
46 + <IndexableDisplayName
47 + displayName={hocDisplayNames[0]}
48 + id={elementID}
49 + />
50 + </Badge>
51 + )}
52 +
53 + {hocDisplayNames != null && hocDisplayNames.length > 1 && (
54 + <div className={styles.ExtraLabel}>+{hocDisplayNames.length - 1}</div>
55 + )}
56 + </div>
57 + );
58 +}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.css new
+9
@@ -0,0 +1,9 @@
1 +.Root {
2 + padding: 0.25rem;
3 + user-select: none;
4 + display: inline-flex;
5 +}
6 +
7 +.Root *:not(:first-child) {
8 + margin-left: 0.25rem;
9 +}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.js new
+36
@@ -0,0 +1,36 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {Element} from 'react-devtools-shared/src/frontend/types';
11 +
12 +import * as React from 'react';
13 +
14 +import Badge from './Badge';
15 +import ForgetBadge from './ForgetBadge';
16 +
17 +import styles from './InspectedElementBadges.css';
18 +
19 +type Props = {
20 + element: Element,
21 +};
22 +
23 +export default function InspectedElementBadges({element}: Props): React.Node {
24 + const {hocDisplayNames, compiledWithForget} = element;
25 +
26 + return (
27 + <div className={styles.Root}>
28 + {compiledWithForget && <ForgetBadge indexable={false} />}
29 +
30 + {hocDisplayNames !== null &&
31 + hocDisplayNames.map(hocDisplayName => (
32 + <Badge key={hocDisplayName}>{hocDisplayName}</Badge>
33 + ))}
34 + </div>
35 + );
36 +}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
+17 -18
@@ -17,7 +17,7 @@ import ContextMenuItem from '../../ContextMenu/ContextMenuItem';
17 import Button from '../Button';
18 import ButtonIcon from '../ButtonIcon';
19 import Icon from '../Icon';
20 -import HocBadges from './HocBadges';
20 +import InspectedElementBadges from './InspectedElementBadges';
21 import InspectedElementContextTree from './InspectedElementContextTree';
22 import InspectedElementErrorsAndWarningsTree from './InspectedElementErrorsAndWarningsTree';
23 import InspectedElementHooksTree from './InspectedElementHooksTree';
@@ -26,7 +26,7 @@ import InspectedElementStateTree from './InspectedElementStateTree';
26 import InspectedElementStyleXPlugin from './InspectedElementStyleXPlugin';
27 import InspectedElementSuspenseToggle from './InspectedElementSuspenseToggle';
28 import NativeStyleEditor from './NativeStyleEditor';
29 -import Badge from './Badge';
29 +import ElementBadges from './ElementBadges';
30 import {useHighlightNativeElement} from '../hooks';
31 import {
32 copyInspectedElementPath as copyInspectedElementPathAPI,
@@ -41,12 +41,8 @@ import type {ContextMenuContextType} from '../context';
41 import type {
42 Element,
43 InspectedElement,
44 - SerializedElement,
45 -} from 'react-devtools-shared/src/frontend/types';
46 -import type {
47 - ElementType,
48 - HookNames,
44 } from 'react-devtools-shared/src/frontend/types';
45 +import type {HookNames} from 'react-devtools-shared/src/frontend/types';
46 import type {ToggleParseHookNames} from './InspectedElementContext';
47
48 export type CopyPath = (path: Array<string | number>) => void;
@@ -90,7 +86,7 @@ export default function InspectedElementView({
86 return (
87 <Fragment>
88 <div className={styles.InspectedElement}>
93 - <HocBadges element={element} />
89 + <InspectedElementBadges element={element} />
90
91 <InspectedElementPropsTree
92 bridge={bridge}
@@ -152,17 +148,20 @@ export default function InspectedElementView({
148 className={styles.Owners}
149 data-testname="InspectedElementView-Owners">
150 <div className={styles.OwnersHeader}>rendered by</div>
151 +
152 {showOwnersList &&
156 - ((owners: any): Array<SerializedElement>).map(owner => (
153 + owners?.map(owner => (
154 <OwnerView
155 key={owner.id}
156 displayName={owner.displayName || 'Anonymous'}
157 hocDisplayNames={owner.hocDisplayNames}
158 + compiledWithForget={owner.compiledWithForget}
159 id={owner.id}
160 isInStore={store.containsElement(owner.id)}
161 type={owner.type}
162 />
163 ))}
164 +
165 {rootType !== null && (
166 <div className={styles.OwnersMetaField}>{rootType}</div>
167 )}
@@ -286,17 +285,17 @@ function Source({fileName, lineNumber}: SourceProps) {
285 type OwnerViewProps = {
286 displayName: string,
287 hocDisplayNames: Array<string> | null,
288 + compiledWithForget: boolean,
289 id: number,
290 isInStore: boolean,
291 - type: ElementType,
291 };
292
293 function OwnerView({
294 displayName,
295 hocDisplayNames,
296 + compiledWithForget,
297 id,
298 isInStore,
299 - type,
299 }: OwnerViewProps) {
300 const dispatch = useContext(TreeDispatcherContext);
301 const {highlightNativeElement, clearHighlightNativeElement} =
@@ -313,25 +312,25 @@ function OwnerView({
312 });
313 }, [dispatch, id]);
314
316 - const onMouseEnter = () => highlightNativeElement(id);
317 -
318 - const onMouseLeave = clearHighlightNativeElement;
319 -
315 return (
316 <Button
317 key={id}
318 className={styles.OwnerButton}
319 disabled={!isInStore}
320 onClick={handleClick}
326 - onMouseEnter={onMouseEnter}
327 - onMouseLeave={onMouseLeave}>
321 + onMouseEnter={() => highlightNativeElement(id)}
322 + onMouseLeave={clearHighlightNativeElement}>
323 <span className={styles.OwnerContent}>
324 <span
325 className={`${styles.Owner} ${isInStore ? '' : styles.NotInStore}`}
326 title={displayName}>
327 {displayName}
328 </span>
334 - <Badge hocDisplayNames={hocDisplayNames} type={type} />
329 +
330 + <ElementBadges
331 + hocDisplayNames={hocDisplayNames}
332 + compiledWithForget={compiledWithForget}
333 + />
334 </span>
335 </Button>
336 );
packages/react-devtools-shared/src/devtools/views/Components/OwnersListContext.js
+2 -11
@@ -14,7 +14,7 @@ import {createContext, useCallback, useContext, useEffect} from 'react';
14 import {createResource} from '../../cache';
15 import {BridgeContext, StoreContext} from '../context';
16 import {TreeStateContext} from './TreeContext';
17 -import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
17 +import {backendToFrontendSerializedElementMapper} from 'react-devtools-shared/src/utils';
18
19 import type {OwnersList} from 'react-devtools-shared/src/backend/types';
20 import type {
@@ -100,16 +100,7 @@ function OwnersListContextController({children}: Props): React.Node {
100 request.resolveFn(
101 ownersList.owners === null
102 ? null
103 - : ownersList.owners.map(owner => {
104 - const [displayNameWithoutHOCs, hocDisplayNames] =
105 - separateDisplayNameAndHOCs(owner.displayName, owner.type);
106 -
107 - return {
108 - ...owner,
109 - displayName: displayNameWithoutHOCs,
110 - hocDisplayNames,
111 - };
112 - }),
103 + : ownersList.owners.map(backendToFrontendSerializedElementMapper),
104 );
105 }
106 }
packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.css
+1 -1
@@ -99,6 +99,6 @@
99 color: var(--color-dimmest);
100 }
101
102 -.Badge {
102 +.BadgesBlock {
103 margin-left: 0.25rem;
104 }
packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js
+9 -13
@@ -19,7 +19,7 @@ import {
19 import Button from '../Button';
20 import ButtonIcon from '../ButtonIcon';
21 import Toggle from '../Toggle';
22 -import Badge from './Badge';
22 +import ElementBadges from './ElementBadges';
23 import {OwnersListContext} from './OwnersListContext';
24 import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
25 import {useIsOverflowing} from '../hooks';
@@ -204,11 +204,7 @@ type ElementsDropdownProps = {
204 selectOwner: SelectOwner,
205 ...
206 };
207 -function ElementsDropdown({
208 - owners,
209 - selectedIndex,
210 - selectOwner,
211 -}: ElementsDropdownProps) {
207 +function ElementsDropdown({owners, selectOwner}: ElementsDropdownProps) {
208 const store = useContext(StoreContext);
209
210 const menuItems = [];
@@ -222,10 +218,10 @@ function ElementsDropdown({
218 onSelect={() => (isInStore ? selectOwner(owner) : null)}>
219 {owner.displayName}
220
225 - <Badge
226 - className={styles.Badge}
221 + <ElementBadges
222 hocDisplayNames={owner.hocDisplayNames}
228 - type={owner.type}
223 + compiledWithForget={owner.compiledWithForget}
224 + className={styles.BadgesBlock}
225 />
226 </MenuItem>,
227 );
@@ -254,7 +250,7 @@ type ElementViewProps = {
250 function ElementView({isSelected, owner, selectOwner}: ElementViewProps) {
251 const store = useContext(StoreContext);
252
257 - const {displayName, hocDisplayNames, type} = owner;
253 + const {displayName, hocDisplayNames, compiledWithForget} = owner;
254 const isInStore = store.containsElement(owner.id);
255
256 const handleChange = useCallback(() => {
@@ -270,10 +266,10 @@ function ElementView({isSelected, owner, selectOwner}: ElementViewProps) {
266 onChange={handleChange}>
267 {displayName}
268
273 - <Badge
274 - className={styles.Badge}
269 + <ElementBadges
270 hocDisplayNames={hocDisplayNames}
276 - type={type}
271 + compiledWithForget={compiledWithForget}
272 + className={styles.BadgesBlock}
273 />
274 </Toggle>
275 );
packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js
+8 -3
@@ -993,10 +993,13 @@ function recursivelySearchTree(
993 regExp: RegExp,
994 searchResults: Array<number>,
995 ): void {
996 - const {children, displayName, hocDisplayNames} = ((store.getElementByID(
997 - elementID,
998 - ): any): Element);
996 + const element = store.getElementByID(elementID);
997
998 + if (element == null) {
999 + return;
1000 + }
1001 +
1002 + const {children, displayName, hocDisplayNames, compiledWithForget} = element;
1003 if (displayName != null && regExp.test(displayName) === true) {
1004 searchResults.push(elementID);
1005 } else if (
@@ -1005,6 +1008,8 @@ function recursivelySearchTree(
1008 hocDisplayNames.some(name => regExp.test(name)) === true
1009 ) {
1010 searchResults.push(elementID);
1011 + } else if (compiledWithForget && regExp.test('Forget')) {
1012 + searchResults.push(elementID);
1013 }
1014
1015 children.forEach(childID =>
packages/react-devtools-shared/src/devtools/views/Profiler/utils.js
+4 -15
@@ -8,7 +8,7 @@
8 */
9
10 import {PROFILER_EXPORT_VERSION} from 'react-devtools-shared/src/constants';
11 -import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
11 +import {backendToFrontendSerializedElementMapper} from 'react-devtools-shared/src/utils';
12
13 import type {ProfilingDataBackend} from 'react-devtools-shared/src/backend/types';
14 import type {
@@ -106,20 +106,9 @@ export function prepareProfilingDataFrontendFromBackendAndStore(
106 timestamp: commitDataBackend.timestamp,
107 updaters:
108 commitDataBackend.updaters !== null
109 - ? commitDataBackend.updaters.map(serializedElement => {
110 - const [
111 - serializedElementDisplayName,
112 - serializedElementHocDisplayNames,
113 - ] = separateDisplayNameAndHOCs(
114 - serializedElement.displayName,
115 - serializedElement.type,
116 - );
117 - return {
118 - ...serializedElement,
119 - displayName: serializedElementDisplayName,
120 - hocDisplayNames: serializedElementHocDisplayNames,
121 - };
122 - })
109 + ? commitDataBackend.updaters.map(
110 + backendToFrontendSerializedElementMapper,
111 + )
112 : null,
113 }),
114 );
packages/react-devtools-shared/src/frontend/types.js
+5
@@ -151,6 +151,10 @@ export type Element = {
151 // This element is not in a StrictMode compliant subtree.
152 // Only true for React versions supporting StrictMode.
153 isStrictModeNonCompliant: boolean,
154 +
155 + // If component is compiled with Forget, the backend will send its name as Forget(...)
156 + // Later, on the frontend side, we will strip HOC names and Forget prefix.
157 + compiledWithForget: boolean,
158 };
159
160 export type SerializedElement = {
@@ -158,6 +162,7 @@ export type SerializedElement = {
162 id: number,
163 key: number | string | null,
164 hocDisplayNames: Array<string> | null,
165 + compiledWithForget: boolean,
166 type: ElementType,
167 };
168
packages/react-devtools-shared/src/utils.js
+46 -7
@@ -60,8 +60,10 @@ import type {
60 ComponentFilter,
61 ElementType,
62 BrowserTheme,
63 -} from './frontend/types';
64 -import type {LRUCache} from 'react-devtools-shared/src/frontend/types';
63 + SerializedElement as SerializedElementFrontend,
64 + LRUCache,
65 +} from 'react-devtools-shared/src/frontend/types';
66 +import type {SerializedElement as SerializedElementBackend} from 'react-devtools-shared/src/backend/types';
67
68 // $FlowFixMe[method-unbinding]
69 const hasOwnProperty = Object.prototype.hasOwnProperty;
@@ -415,16 +417,35 @@ export function getOpenInEditorURL(): string {
417 return getDefaultOpenInEditorURL();
418 }
419
418 -export function separateDisplayNameAndHOCs(
420 +type ParseElementDisplayNameFromBackendReturn = {
421 + formattedDisplayName: string | null,
422 + hocDisplayNames: Array<string> | null,
423 + compiledWithForget: boolean,
424 +};
425 +export function parseElementDisplayNameFromBackend(
426 displayName: string | null,
427 type: ElementType,
421 -): [string | null, Array<string> | null] {
428 +): ParseElementDisplayNameFromBackendReturn {
429 if (displayName === null) {
423 - return [null, null];
430 + return {
431 + formattedDisplayName: null,
432 + hocDisplayNames: null,
433 + compiledWithForget: false,
434 + };
435 }
436
426 - let hocDisplayNames = null;
437 + if (displayName.startsWith('Forget(')) {
438 + const displayNameWithoutForgetWrapper = displayName.slice(
439 + 7,
440 + displayName.length - 1,
441 + );
442
443 + const {formattedDisplayName, hocDisplayNames} =
444 + parseElementDisplayNameFromBackend(displayNameWithoutForgetWrapper, type);
445 + return {formattedDisplayName, hocDisplayNames, compiledWithForget: true};
446 + }
447 +
448 + let hocDisplayNames = null;
449 switch (type) {
450 case ElementTypeClass:
451 case ElementTypeForwardRef:
@@ -442,7 +463,11 @@ export function separateDisplayNameAndHOCs(
463 break;
464 }
465
445 - return [displayName, hocDisplayNames];
466 + return {
467 + formattedDisplayName: displayName,
468 + hocDisplayNames,
469 + compiledWithForget: false,
470 + };
471 }
472
473 // Pulled from react-compat
@@ -897,3 +922,17 @@ export const isPlainObject = (object: Object): boolean => {
922 const objectParentPrototype = Object.getPrototypeOf(objectPrototype);
923 return !objectParentPrototype;
924 };
925 +
926 +export function backendToFrontendSerializedElementMapper(
927 + element: SerializedElementBackend,
928 +): SerializedElementFrontend {
929 + const {formattedDisplayName, hocDisplayNames, compiledWithForget} =
930 + parseElementDisplayNameFromBackend(element.displayName, element.type);
931 +
932 + return {
933 + ...element,
934 + displayName: formattedDisplayName,
935 + hocDisplayNames,
936 + compiledWithForget,
937 + };
938 +}