Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix devtools displaying Anonymous for memo of ref-forwarding components #17274

Merged
merged 5 commits into from
Nov 5, 2019
Merged
Show file tree
Hide file tree
Changes from 3 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
28 changes: 20 additions & 8 deletions packages/react-devtools-shared/src/backend/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
patch as patchConsole,
registerRenderer as registerRendererWithConsole,
} from './console';
import {isMemo, isForwardRef} from 'react-is';

import type {Fiber} from 'react-reconciler/src/ReactFiber';
import type {
Expand Down Expand Up @@ -326,17 +327,29 @@ export function getInternalReactConstants(
SCOPE_SYMBOL_STRING,
} = ReactSymbols;

function resolveFiberType(type: any) {
// This is to support lazy components with a Promise as the type.
// see https://github.com/facebook/react/pull/13397
if (typeof type.then === 'function') {
return type._reactResult;
}
Comment on lines +334 to +338
Copy link
Contributor Author

@wsmd wsmd Nov 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find any references to _reactResult in the codebase. Is this still relevant? Is it safe to remove?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it has sense been renamed to _result now that you mention it.

Someone can follow up with that in a separate PR.

cc @acdlite

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this codepath is even used by DevTools anymore. By the time React DevTools gets called for the lazy component, it just gets the e.g. function.

if (isForwardRef(type)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should use react-is for this. Partly because the renderer already knows enough information to make this determination using the Fiber's tag and partly because react-is is meant to work with React elements and the renderer has a Fiber with a "type" (which is not an element).

Edit: This has already been addressed.

return type.render;
}
// recursively resolving memo type in case of memo(forwardRef(Component))
if (isMemo(type)) {
return resolveFiberType(type.type);
}
return type;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to add a new test or two to DevTools store-test for the types that were showing up as anonymous before that no longer are, to guard against a regression.

I can add that after merging the fix though :)

}

// NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods
function getDisplayNameForFiber(fiber: Fiber): string | null {
const {elementType, type, tag} = fiber;

// This is to support lazy components with a Promise as the type.
// see https://github.com/facebook/react/pull/13397
let resolvedType = type;
if (typeof type === 'object' && type !== null) {
if (typeof type.then === 'function') {
resolvedType = type._reactResult;
}
resolvedType = resolveFiberType(type);
}

let resolvedContext: any = null;
Expand All @@ -350,8 +363,7 @@ export function getInternalReactConstants(
return getDisplayName(resolvedType);
case ForwardRef:
return (
resolvedType.displayName ||
getDisplayName(resolvedType.render, 'Anonymous')
resolvedType.displayName || getDisplayName(resolvedType, 'Anonymous')
);
case HostRoot:
return null;
Expand All @@ -366,7 +378,7 @@ export function getInternalReactConstants(
if (elementType.displayName) {
return elementType.displayName;
} else {
return getDisplayName(type, 'Anonymous');
return getDisplayName(resolvedType, 'Anonymous');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had been thinking of suggesting a different approach that just kind of localized the check:

diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js
index 761c78935..284183f9f 100644
--- a/packages/react-devtools-shared/src/backend/renderer.js
+++ b/packages/react-devtools-shared/src/backend/renderer.js
@@ -365,6 +365,9 @@ export function getInternalReactConstants(
       case SimpleMemoComponent:
         if (elementType.displayName) {
           return elementType.displayName;
+        } else if (type.type != null && type.type.render != null) {
+          // Special case of a forwardRef() nested in a memo()
+          return getDisplayName(type.type.render, 'Anonymous');
         } else {
           return getDisplayName(type, 'Anonymous');
         }

I don't know that I really have a preference between the two approaches I guess. 😄 Yours is fine~

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests added #17283

}
case SuspenseComponent:
return 'Suspense';
Expand Down
1 change: 1 addition & 0 deletions packages/react-is/src/ReactIs.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export function typeOf(object: any) {
}
case REACT_LAZY_TYPE:
case REACT_MEMO_TYPE:
case REACT_FORWARD_REF_TYPE:
case REACT_PORTAL_TYPE:
return $$typeof;
}
Expand Down
9 changes: 6 additions & 3 deletions packages/react-is/src/__tests__/ReactIs-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ describe('ReactIs', () => {

it('should identify ref forwarding component', () => {
const RefForwardingComponent = React.forwardRef((props, ref) => null);
expect(ReactIs.typeOf(RefForwardingComponent)).toBe(ReactIs.ForwardRef);
expect(ReactIs.typeOf(<RefForwardingComponent />)).toBe(ReactIs.ForwardRef);
expect(ReactIs.isForwardRef(RefForwardingComponent)).toBe(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. This change seems to mirror the current behavior of memo components, so I understand it from that aspect, but I don't think it's right. The typeOf method is meant to look at React element types (e.g. <MyComponent /> or React.createElement(MyComponent)) not component types (e.g. MyComponent).

I think the one exception for this that makes sense may be portals, since the value returned by React.createPortal() is meant to be rendered directly (and not wrapped in an element).

I'm going to file a separate PR for this particular issue since I don't think it's strictly related to this PR: #17278

expect(ReactIs.isForwardRef(<RefForwardingComponent />)).toBe(true);
expect(ReactIs.isForwardRef({type: ReactIs.StrictMode})).toBe(false);
expect(ReactIs.isForwardRef(<div />)).toBe(false);
Expand All @@ -131,9 +133,10 @@ describe('ReactIs', () => {

it('should identify memo', () => {
const Component = () => React.createElement('div');
const memoized = React.memo(Component);
expect(ReactIs.typeOf(memoized)).toBe(ReactIs.Memo);
expect(ReactIs.isMemo(memoized)).toBe(true);
const Memoized = React.memo(Component);
expect(ReactIs.typeOf(Memoized)).toBe(ReactIs.Memo);
expect(ReactIs.typeOf(<Memoized />)).toBe(ReactIs.Element);
wsmd marked this conversation as resolved.
Show resolved Hide resolved
expect(ReactIs.isMemo(Memoized)).toBe(true);
expect(ReactIs.isMemo(Component)).toBe(false);
});

Expand Down