Skip to content

DevTools: Order of higher-order component badges #21939

@eps1lon

Description

@eps1lon
Collaborator

Given

const StyleDiv = forwardRef(function Component({ children }, ref) {
  return <div ref={ref}>{children}</div>;
});

StyleDiv.displayName = `styled(connected(div))`;

-- https://codesandbox.io/s/little-sky-y8h1b?file=/src/App.js

I would expect that the badges from the display name are prioritized in the component tree.
However, devtools currently displays the ForwardRef badge first:

div ForwardRef +2
Screenshot from 2021-07-22 11-44-32

Oddly enough, the inline devtools in codesandbox do prioritize the badge from the displayName (maybe this regressed?):

div styled +2 Screenshot from 2021-07-22 11-45-27

There's also an argument to be made that devtools should not display the ForwardRef badge to begin with (since we explicitly omitted it in displayName). That can be discussed separately but would solve the issue entirely.

Activity

bvaughn

bvaughn commented on Jul 22, 2021

@bvaughn
Contributor

Just an FYI, the HocBadges component (the one that displays badges for the inspected component, in the right panel) just iterates over the list of display names:

return (
<div className={styles.HocBadges}>
{hocDisplayNames !== null &&
hocDisplayNames.map(hocDisplayName => (
<div key={hocDisplayName} className={styles.Badge}>
{hocDisplayName}
</div>
))}
</div>
);

This list of names is calculated using this helper util which first parses the displayName and then unshifts special case forwardRef and memo names:

export function separateDisplayNameAndHOCs(
displayName: string | null,
type: ElementType,
): [string | null, Array<string> | null] {
if (displayName === null) {
return [null, null];
}
let hocDisplayNames = null;
switch (type) {
case ElementTypeClass:
case ElementTypeForwardRef:
case ElementTypeFunction:
case ElementTypeMemo:
if (displayName.indexOf('(') >= 0) {
const matches = displayName.match(/[^()]+/g);
if (matches != null) {
displayName = matches.pop();
hocDisplayNames = matches;
}
}
break;
default:
break;
}
if (type === ElementTypeMemo) {
if (hocDisplayNames === null) {
hocDisplayNames = ['Memo'];
} else {
hocDisplayNames.unshift('Memo');
}
} else if (type === ElementTypeForwardRef) {
if (hocDisplayNames === null) {
hocDisplayNames = ['ForwardRef'];
} else {
hocDisplayNames.unshift('ForwardRef');
}
}
return [displayName, hocDisplayNames];
}

The badge shown in the tree, beside the name, uses this same value and just displays the first name:

{hocDisplayNames !== null && hocDisplayNames.length > 0 ? (
<Badge
className={styles.Badge}
hocDisplayNames={hocDisplayNames}
type={type}>
<DisplayName
displayName={hocDisplayNames[0]}
id={((id: any): number)}
/>
</Badge>
) : null}

Oddly enough, the inline devtools in codesandbox do prioritize the badge from the displayName (maybe this regressed

Code Sandbox uses a pretty old version of DevTools b'c no one has updated it anytime recently and the one time I tried, I was unable to get the project running locally.

Regardless, what order would you expect them to be in? This order kind of seemed reasonable to me but I'm open for discussion. (Let's account for both forwardRef and memo in whatever proposal we discuss.)

eps1lon

eps1lon commented on Jul 23, 2021

@eps1lon
CollaboratorAuthor

Regardless, what order would you expect them to be in? This order kind of seemed reasonable to me but I'm open for discussion. (Let's account for both forwardRef and memo in whatever proposal we discuss.)

I've thought about this more and the order does make sense generally e.g. styled(connected(fancy(logged(Component)))) should display as Component Styled Connected Fancy Logged

I think my issue is more that ForwardRef and memo are displayed even though I've set an explicit displayName. Conceptually in

function styled(Component) {
   const StyledComponent = React.forwardRef(function StyledComponent(props, ref) {
     const className = "styled"; // dummy styles
     return <Component {...props} className={className} ref={ref} />;
   });
   StyledComponent.displayName = `styled(${getDisplayName(Component)})`;
   return StyledComponent;
}

the forwardRef is part of styled. So they're not independent higher-order components but a single one.

The current devtools behavior is confusing for another reason: shared/getComponentNameFromType ignores forwardRef and memo as well if we set a displayName e.g. in

const Component = React.forwardRef(function Component() {});
Component.displayName = `Fancy`;

Component will not have the computed display name ForwardRef(Component) (e.g. in propType warnings) but Component. However, devtools will display it as Component ForwardRef: https://codesandbox.io/s/displayname-devtools-vs-react-itself-if6dg

So I think this is less about the order (otherwise where do we splice in ForwardRef if not at the start?) but more about not displaying ForwardRef at all if we explicitly set a displayName.

Because right now most modern higher-order components (styled-components, react-redux, emotion) do return a forwardRef component but their badge will not appear in the component tree. All of these components are displayed either as forwardRef or memo and I don't find that information helpful. I'd rather see the badges of the higher-order components:

Screenshot from 2021-07-23 10-13-19

-- https://codesandbox.io/s/displayname-of-popular-higher-order-components-l7yw3

bvaughn

bvaughn commented on Jul 23, 2021

@bvaughn
Contributor

I think my issue is more that ForwardRef and memo are displayed even though I've set an explicit displayName. Conceptually in

That's an interesting POV.

Seems like a pretty compelling argument. Want to post a PR?

markerikson

markerikson commented on Jul 24, 2021

@markerikson
Contributor

Yeah, I'd echo this. For libs like these, forwardRef is an implementation detail of the actual HOC, not a primary aspect by itself. Checking for a displayName seems like a reasonable way to figure that out.

(technically connect doesn't actually add a forwardRef unless users specifically opt into that by passing an option, but close enough)

eps1lon

eps1lon commented on Jul 24, 2021

@eps1lon
CollaboratorAuthor

forwardRef is an implementation detail of the actual HOC, not a primary aspect by itself.

That's how I see it as well.

Seems like a pretty compelling argument. Want to post a PR?

My pleasure.

bvaughn

bvaughn commented on Jul 26, 2021

@bvaughn
Contributor

I think the linked PR isn't quite what I thought we'd settled on in the discussion above.

I don't think we should change e.g. memo from a badge (Component [memo]) to a wrapper string (Memo(Component)). I thought we said we'd with the badge, except for when a displayName is specified, in which case we should use it instead?

I think the comparison to packages/shared/getComponentNameFromType is a little bit of an apples-to-oranges comparison, since that's a plain text warning and the DevTools Components tree is a rich UI.

That being said, I can also see an argument for React.memo and React.forwardRef being substantially different from HOCs and needing a different visual treatment as a result. Maybe the solution would be to not show a [memo] or [forwardRef] badge in the tree view at all, but show it in the inspected props on the right hand side when selected? (It may be an "implementation detail" but that can still be useful information when debugging things, no?)

How would you feel about doing this instead?

eps1lon

eps1lon commented on Jul 26, 2021

@eps1lon
CollaboratorAuthor

I don't think we should change e.g. memo from a badge (Component [memo]) to a wrapper string (Memo(Component)). I thought we said we'd with the badge, except for when a displayName is specified, in which case we should use it instead?

That was the goal of the PR. I think it's just not immediately obvious that that's the change. But we can discuss this in the PR.

I think the comparison to packages/shared/getComponentNameFromType is a little bit of an apples-to-oranges comparison, since that's a plain text warning and the DevTools Components tree is a rich UI.

The problem is that these things can usually be mapped onto another. And how each treats memo/forwardRef right now makes the mapping harder than it could be.

(It may be an "implementation detail" but that can still be useful information when debugging things, no?)

For library authors maybe but then we could always add the memo(...) ourselves.

Though I wouldn't mind having an extra signifier that a certain component is wrapped in memo or forwardRef (or even rendered via lazy?).

bvaughn

bvaughn commented on Jul 26, 2021

@bvaughn
Contributor

For library authors maybe but then we could always add the memo(...) ourselves.

I think it could be useful info for more than just library authors (e.g. a "memo" indicator would explain why a component isn't re-rendering when things around it are). Probably not useful enough to be in the main tree view, but in the inspected panel on the side?

I think we could use the same visual area, but just style the memo/forwardRef badges differently (different color).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Participants

      @bvaughn@markerikson@eps1lon

      Issue actions

        DevTools: Order of higher-order component badges · Issue #21939 · facebook/react