Skip to content

Commit a37ce1d

Browse files
author
Brian Vaughn
committed
Unify React.memo and React.forwardRef display name logic
1 parent 546c243 commit a37ce1d

File tree

8 files changed

+149
-64
lines changed

8 files changed

+149
-64
lines changed

packages/react-devtools-shared/src/__tests__/__snapshots__/store-test.js.snap

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -670,20 +670,3 @@ exports[`Store should properly serialize non-string key values: 1: mount 1`] = `
670670
[root]
671671
<Child key="123">
672672
`;
673-
674-
exports[`Store should show the right display names for special component types 1`] = `
675-
[root]
676-
▾ <App>
677-
<MyComponent>
678-
<MyComponent> [ForwardRef]
679-
▾ <Anonymous> [ForwardRef]
680-
<MyComponent2>
681-
<Custom> [ForwardRef]
682-
<MyComponent4> [Memo]
683-
▾ <MyComponent> [Memo]
684-
<MyComponent> [ForwardRef]
685-
<Baz> [withFoo][withBar]
686-
<Baz> [Memo][withFoo][withBar]
687-
<Baz> [ForwardRef][withFoo][withBar]
688-
<Cache>
689-
`;

packages/react-devtools-shared/src/__tests__/store-test.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,17 @@ describe('Store', () => {
879879
FakeHigherOrderComponent,
880880
);
881881

882+
const MemoizedFakeHigherOrderComponentWithDisplayNameOverride = React.memo(
883+
FakeHigherOrderComponent,
884+
);
885+
MemoizedFakeHigherOrderComponentWithDisplayNameOverride.displayName =
886+
'memoRefOverride';
887+
const ForwardRefFakeHigherOrderComponentWithDisplayNameOverride = React.forwardRef(
888+
FakeHigherOrderComponent,
889+
);
890+
ForwardRefFakeHigherOrderComponentWithDisplayNameOverride.displayName =
891+
'forwardRefOverride';
892+
882893
const App = () => (
883894
<React.Fragment>
884895
<MyComponent />
@@ -891,6 +902,8 @@ describe('Store', () => {
891902
<MemoizedFakeHigherOrderComponent />
892903
<ForwardRefFakeHigherOrderComponent />
893904
<React.unstable_Cache />
905+
<MemoizedFakeHigherOrderComponentWithDisplayNameOverride />
906+
<ForwardRefFakeHigherOrderComponentWithDisplayNameOverride />
894907
</React.Fragment>
895908
);
896909

@@ -904,7 +917,24 @@ describe('Store', () => {
904917
// Render again after it resolves
905918
act(() => ReactDOM.render(<App />, container));
906919

907-
expect(store).toMatchSnapshot();
920+
expect(store).toMatchInlineSnapshot(`
921+
[root]
922+
▾ <App>
923+
<MyComponent>
924+
<MyComponent> [ForwardRef]
925+
▾ <Anonymous> [ForwardRef]
926+
<MyComponent2>
927+
<Custom> [ForwardRef]
928+
<MyComponent4> [Memo]
929+
▾ <MyComponent> [Memo]
930+
<MyComponent> [ForwardRef]
931+
<Baz> [withFoo][withBar]
932+
<Baz> [Memo][withFoo][withBar]
933+
<Baz> [ForwardRef][withFoo][withBar]
934+
<Cache>
935+
<memoRefOverride> [Memo]
936+
<forwardRefOverride> [ForwardRef]
937+
`);
908938
});
909939

910940
describe('Lazy', () => {

packages/react-devtools-shared/src/backend/renderer.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ export function getInternalReactConstants(
393393

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

398398
let resolvedType = type;
399399
if (typeof type === 'object' && type !== null) {
@@ -432,7 +432,11 @@ export function getInternalReactConstants(
432432
return 'Lazy';
433433
case MemoComponent:
434434
case SimpleMemoComponent:
435-
return getDisplayName(resolvedType, 'Anonymous');
435+
return (
436+
(elementType && elementType.displayName) ||
437+
(type && type.displayName) ||
438+
getDisplayName(resolvedType, 'Anonymous')
439+
);
436440
case SuspenseComponent:
437441
return 'Suspense';
438442
case LegacyHiddenComponent:

packages/react-reconciler/src/__tests__/ReactMemo-test.js

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -498,11 +498,8 @@ describe('memo', () => {
498498
});
499499
});
500500

501-
it('should honor a displayName if set on the memo wrapper in warnings', () => {
502-
const MemoComponent = React.memo(function Component(props) {
503-
return <div {...props} />;
504-
});
505-
MemoComponent.displayName = 'Foo';
501+
it('should fall back to showing something meaningful if no displayName or name are present', () => {
502+
const MemoComponent = React.memo(props => <div {...props} />);
506503
MemoComponent.propTypes = {
507504
required: PropTypes.string.isRequired,
508505
};
@@ -511,19 +508,20 @@ describe('memo', () => {
511508
ReactNoop.render(<MemoComponent optional="foo" />),
512509
).toErrorDev(
513510
'Warning: Failed prop type: The prop `required` is marked as required in ' +
514-
'`Foo`, but its value is `undefined`.\n' +
515-
' in Foo (at **)',
511+
'`Memo`, but its value is `undefined`.',
512+
// There's no component stack in this warning because the inner function is anonymous.
513+
// If we wanted to support this (for the Error frames / source location)
514+
// we could do this by updating ReactComponentStackFrame.
515+
{withoutStack: true},
516516
);
517517
});
518518

519-
it('should honor a outter displayName when wrapped component and memo component set displayName at the same time.', () => {
519+
it('should honor a displayName if set on the inner component in warnings', () => {
520520
function Component(props) {
521521
return <div {...props} />;
522522
}
523-
Component.displayName = 'Foo';
524-
523+
Component.displayName = 'Inner';
525524
const MemoComponent = React.memo(Component);
526-
MemoComponent.displayName = 'Bar';
527525
MemoComponent.propTypes = {
528526
required: PropTypes.string.isRequired,
529527
};
@@ -532,21 +530,37 @@ describe('memo', () => {
532530
ReactNoop.render(<MemoComponent optional="foo" />),
533531
).toErrorDev(
534532
'Warning: Failed prop type: The prop `required` is marked as required in ' +
535-
'`Bar`, but its value is `undefined`.\n' +
536-
' in Bar (at **)',
533+
'`Inner`, but its value is `undefined`.\n' +
534+
' in Inner (at **)',
535+
);
536+
});
537+
538+
it('should honor a displayName if set on the memo wrapper in warnings', () => {
539+
const MemoComponent = React.memo(function Component(props) {
540+
return <div {...props} />;
541+
});
542+
MemoComponent.displayName = 'Outer';
543+
MemoComponent.propTypes = {
544+
required: PropTypes.string.isRequired,
545+
};
546+
547+
expect(() =>
548+
ReactNoop.render(<MemoComponent optional="foo" />),
549+
).toErrorDev(
550+
'Warning: Failed prop type: The prop `required` is marked as required in ' +
551+
'`Outer`, but its value is `undefined`.\n' +
552+
' in Component (at **)',
537553
);
538554
});
539555

540-
it('can set react memo component displayName multiple times', () => {
556+
it('should honor a outer displayName when wrapped component and memo component set displayName at the same time.', () => {
541557
function Component(props) {
542558
return <div {...props} />;
543559
}
544-
Component.displayName = 'Foo';
560+
Component.displayName = 'Inner';
545561

546562
const MemoComponent = React.memo(Component);
547-
MemoComponent.displayName = 'MemoComp01';
548-
MemoComponent.displayName = 'MemoComp02';
549-
MemoComponent.displayName = 'MemoComp03';
563+
MemoComponent.displayName = 'Outer';
550564
MemoComponent.propTypes = {
551565
required: PropTypes.string.isRequired,
552566
};
@@ -555,8 +569,8 @@ describe('memo', () => {
555569
ReactNoop.render(<MemoComponent optional="foo" />),
556570
).toErrorDev(
557571
'Warning: Failed prop type: The prop `required` is marked as required in ' +
558-
'`MemoComp03`, but its value is `undefined`.\n' +
559-
' in MemoComp03 (at **)',
572+
'`Outer`, but its value is `undefined`.\n' +
573+
' in Inner (at **)',
560574
);
561575
});
562576
}

packages/react/src/ReactForwardRef.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,6 @@ export function forwardRef<Props, ElementType: React$ElementType>(
5757
},
5858
set: function(name) {
5959
ownName = name;
60-
if (render.displayName == null) {
61-
render.displayName = name;
62-
}
6360
},
6461
});
6562
}

packages/react/src/ReactMemo.js

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,7 @@ export function memo<Props>(
3636
return ownName;
3737
},
3838
set: function(name) {
39-
if (typeof name === 'string') {
40-
ownName = name;
41-
type.displayName = name;
42-
} else {
43-
console.error(
44-
"%s: is not valid displayName type, React memo's displayName should be a string",
45-
typeof name,
46-
);
47-
}
39+
ownName = name;
4840
},
4941
});
5042
}

packages/react/src/__tests__/forwardRef-test.js

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -217,14 +217,43 @@ describe('forwardRef', () => {
217217
);
218218
});
219219

220-
it('should honor a displayName if set on the forwardRef wrapper in warnings', () => {
220+
it('should fall back to showing something meaningful if no displayName or name are present', () => {
221221
const Component = props => <div {...props} />;
222222

223223
const RefForwardingComponent = React.forwardRef((props, ref) => (
224224
<Component {...props} forwardedRef={ref} />
225225
));
226226

227-
RefForwardingComponent.displayName = 'Foo';
227+
RefForwardingComponent.propTypes = {
228+
optional: PropTypes.string,
229+
required: PropTypes.string.isRequired,
230+
};
231+
232+
RefForwardingComponent.defaultProps = {
233+
optional: 'default',
234+
};
235+
236+
const ref = React.createRef();
237+
238+
expect(() =>
239+
ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />),
240+
).toErrorDev(
241+
'Warning: Failed prop type: The prop `required` is marked as required in ' +
242+
'`ForwardRef`, but its value is `undefined`.',
243+
// There's no component stack in this warning because the inner function is anonymous.
244+
// If we wanted to support this (for the Error frames / source location)
245+
// we could do this by updating ReactComponentStackFrame.
246+
{withoutStack: true},
247+
);
248+
});
249+
250+
it('should honor a displayName if set on the forwardRef wrapper in warnings', () => {
251+
const Component = props => <div {...props} />;
252+
253+
const RefForwardingComponent = React.forwardRef((props, ref) => (
254+
<Component {...props} forwardedRef={ref} />
255+
));
256+
RefForwardingComponent.displayName = 'Outer';
228257

229258
RefForwardingComponent.propTypes = {
230259
optional: PropTypes.string,
@@ -241,17 +270,48 @@ describe('forwardRef', () => {
241270
ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />),
242271
).toErrorDev(
243272
'Warning: Failed prop type: The prop `required` is marked as required in ' +
244-
'`Foo`, but its value is `undefined`.\n' +
245-
' in Foo (at **)',
273+
'`Outer`, but its value is `undefined`.',
274+
// There's no component stack in this warning because the inner function is anonymous.
275+
// If we wanted to support this (for the Error frames / source location)
276+
// we could do this by updating ReactComponentStackFrame.
277+
{withoutStack: true},
246278
);
247279
});
248280

249281
it('should honor a displayName in stacks if set on the inner function', () => {
250282
const Component = props => <div {...props} />;
251283

252284
const inner = (props, ref) => <Component {...props} forwardedRef={ref} />;
253-
inner.displayName = 'Foo';
285+
inner.displayName = 'Inner';
286+
const RefForwardingComponent = React.forwardRef(inner);
287+
288+
RefForwardingComponent.propTypes = {
289+
optional: PropTypes.string,
290+
required: PropTypes.string.isRequired,
291+
};
292+
293+
RefForwardingComponent.defaultProps = {
294+
optional: 'default',
295+
};
296+
297+
const ref = React.createRef();
298+
299+
expect(() =>
300+
ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />),
301+
).toErrorDev(
302+
'Warning: Failed prop type: The prop `required` is marked as required in ' +
303+
'`ForwardRef(Inner)`, but its value is `undefined`.\n' +
304+
' in Inner (at **)',
305+
);
306+
});
307+
308+
it('should honor a outer displayName when wrapped component and memo component set displayName at the same time.', () => {
309+
const Component = props => <div {...props} />;
310+
311+
const inner = (props, ref) => <Component {...props} forwardedRef={ref} />;
312+
inner.displayName = 'Inner';
254313
const RefForwardingComponent = React.forwardRef(inner);
314+
RefForwardingComponent.displayName = 'Outer';
255315

256316
RefForwardingComponent.propTypes = {
257317
optional: PropTypes.string,
@@ -268,8 +328,8 @@ describe('forwardRef', () => {
268328
ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />),
269329
).toErrorDev(
270330
'Warning: Failed prop type: The prop `required` is marked as required in ' +
271-
'`ForwardRef(Foo)`, but its value is `undefined`.\n' +
272-
' in Foo (at **)',
331+
'`Outer`, but its value is `undefined`.\n' +
332+
' in Inner (at **)',
273333
);
274334
});
275335

packages/shared/getComponentNameFromType.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ function getWrappedName(
3131
innerType: any,
3232
wrapperName: string,
3333
): string {
34+
const displayName = (outerType: any).displayName;
35+
if (displayName) {
36+
return displayName;
37+
}
3438
const functionName = innerType.displayName || innerType.name || '';
35-
return (
36-
(outerType: any).displayName ||
37-
(functionName !== '' ? `${wrapperName}(${functionName})` : wrapperName)
38-
);
39+
return functionName !== '' ? `${wrapperName}(${functionName})` : wrapperName;
3940
}
4041

4142
// Keep in sync with react-reconciler/getComponentNameFromFiber
@@ -90,7 +91,11 @@ export default function getComponentNameFromType(type: mixed): string | null {
9091
case REACT_FORWARD_REF_TYPE:
9192
return getWrappedName(type, type.render, 'ForwardRef');
9293
case REACT_MEMO_TYPE:
93-
return getComponentNameFromType(type.type);
94+
const outerName = (type: any).displayName || null;
95+
if (outerName !== null) {
96+
return outerName;
97+
}
98+
return getComponentNameFromType(type.type) || 'Memo';
9499
case REACT_LAZY_TYPE: {
95100
const lazyComponent: LazyComponent<any, any> = (type: any);
96101
const payload = lazyComponent._payload;

0 commit comments

Comments
 (0)