Skip to content

Commit d1542de

Browse files
Brian VaughniChenLei
Brian Vaughn
andauthored
Unify React.memo and React.forwardRef display name logic (#21392)
Co-authored-by: iChenLei <[email protected]>
1 parent d19257b commit d1542de

File tree

8 files changed

+220
-42
lines changed

8 files changed

+220
-42
lines changed

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

-17
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

+31-1
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

+6-2
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

+63-8
Original file line numberDiff line numberDiff line change
@@ -498,11 +498,66 @@ describe('memo', () => {
498498
});
499499
});
500500

501+
it('should fall back to showing something meaningful if no displayName or name are present', () => {
502+
const MemoComponent = React.memo(props => <div {...props} />);
503+
MemoComponent.propTypes = {
504+
required: PropTypes.string.isRequired,
505+
};
506+
507+
expect(() =>
508+
ReactNoop.render(<MemoComponent optional="foo" />),
509+
).toErrorDev(
510+
'Warning: Failed prop type: The prop `required` is marked as required in ' +
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},
516+
);
517+
});
518+
519+
it('should honor a displayName if set on the inner component in warnings', () => {
520+
function Component(props) {
521+
return <div {...props} />;
522+
}
523+
Component.displayName = 'Inner';
524+
const MemoComponent = React.memo(Component);
525+
MemoComponent.propTypes = {
526+
required: PropTypes.string.isRequired,
527+
};
528+
529+
expect(() =>
530+
ReactNoop.render(<MemoComponent optional="foo" />),
531+
).toErrorDev(
532+
'Warning: Failed prop type: The prop `required` is marked as required in ' +
533+
'`Inner`, but its value is `undefined`.\n' +
534+
' in Inner (at **)',
535+
);
536+
});
537+
501538
it('should honor a displayName if set on the memo wrapper in warnings', () => {
502539
const MemoComponent = React.memo(function Component(props) {
503540
return <div {...props} />;
504541
});
505-
MemoComponent.displayName = 'Foo';
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 **)',
553+
);
554+
});
555+
556+
it('should pass displayName to an anonymous inner component so it shows up in component stacks', () => {
557+
const MemoComponent = React.memo(props => {
558+
return <div {...props} />;
559+
});
560+
MemoComponent.displayName = 'Memo';
506561
MemoComponent.propTypes = {
507562
required: PropTypes.string.isRequired,
508563
};
@@ -511,19 +566,19 @@ describe('memo', () => {
511566
ReactNoop.render(<MemoComponent optional="foo" />),
512567
).toErrorDev(
513568
'Warning: Failed prop type: The prop `required` is marked as required in ' +
514-
'`Foo`, but its value is `undefined`.\n' +
515-
' in Foo (at **)',
569+
'`Memo`, but its value is `undefined`.\n' +
570+
' in Memo (at **)',
516571
);
517572
});
518573

519-
it('should honor a inner displayName if set on the wrapped function', () => {
574+
it('should honor a outer displayName when wrapped component and memo component set displayName at the same time.', () => {
520575
function Component(props) {
521576
return <div {...props} />;
522577
}
523-
Component.displayName = 'Foo';
578+
Component.displayName = 'Inner';
524579

525580
const MemoComponent = React.memo(Component);
526-
MemoComponent.displayName = 'Bar';
581+
MemoComponent.displayName = 'Outer';
527582
MemoComponent.propTypes = {
528583
required: PropTypes.string.isRequired,
529584
};
@@ -532,8 +587,8 @@ describe('memo', () => {
532587
ReactNoop.render(<MemoComponent optional="foo" />),
533588
).toErrorDev(
534589
'Warning: Failed prop type: The prop `required` is marked as required in ' +
535-
'`Foo`, but its value is `undefined`.\n' +
536-
' in Foo (at **)',
590+
'`Outer`, but its value is `undefined`.\n' +
591+
' in Inner (at **)',
537592
);
538593
});
539594
}

packages/react/src/ReactForwardRef.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,15 @@ export function forwardRef<Props, ElementType: React$ElementType>(
5757
},
5858
set: function(name) {
5959
ownName = name;
60-
if (render.displayName == null) {
60+
61+
// The inner component shouldn't inherit this display name in most cases,
62+
// because the component may be used elsewhere.
63+
// But it's nice for anonymous functions to inherit the name,
64+
// so that our component-stack generation logic will display their frames.
65+
// An anonymous function generally suggests a pattern like:
66+
// React.forwardRef((props, ref) => {...});
67+
// This kind of inner function is not used elsewhere so the side effect is okay.
68+
if (!render.name && !render.displayName) {
6169
render.displayName = name;
6270
}
6371
},

packages/react/src/ReactMemo.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,15 @@ export function memo<Props>(
3737
},
3838
set: function(name) {
3939
ownName = name;
40-
if (type.displayName == null) {
40+
41+
// The inner component shouldn't inherit this display name in most cases,
42+
// because the component may be used elsewhere.
43+
// But it's nice for anonymous functions to inherit the name,
44+
// so that our component-stack generation logic will display their frames.
45+
// An anonymous function generally suggests a pattern like:
46+
// React.memo((props) => {...});
47+
// This kind of inner function is not used elsewhere so the side effect is okay.
48+
if (!type.name && !type.displayName) {
4149
type.displayName = name;
4250
}
4351
},

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

+92-7
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(function Inner(props, ref) {
254+
<Component {...props} forwardedRef={ref} />;
255+
});
256+
RefForwardingComponent.displayName = 'Custom';
228257

229258
RefForwardingComponent.propTypes = {
230259
optional: PropTypes.string,
@@ -241,17 +270,73 @@ 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+
'`Custom`, but its value is `undefined`.\n' +
274+
' in Inner (at **)',
275+
);
276+
});
277+
278+
it('should pass displayName to an anonymous inner component so it shows up in component stacks', () => {
279+
const Component = props => <div {...props} />;
280+
281+
const RefForwardingComponent = React.forwardRef((props, ref) => (
282+
<Component {...props} forwardedRef={ref} />
283+
));
284+
RefForwardingComponent.displayName = 'Custom';
285+
286+
RefForwardingComponent.propTypes = {
287+
optional: PropTypes.string,
288+
required: PropTypes.string.isRequired,
289+
};
290+
291+
RefForwardingComponent.defaultProps = {
292+
optional: 'default',
293+
};
294+
295+
const ref = React.createRef();
296+
297+
expect(() =>
298+
ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />),
299+
).toErrorDev(
300+
'Warning: Failed prop type: The prop `required` is marked as required in ' +
301+
'`Custom`, but its value is `undefined`.\n' +
302+
' in Custom (at **)',
246303
);
247304
});
248305

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

252309
const inner = (props, ref) => <Component {...props} forwardedRef={ref} />;
253-
inner.displayName = 'Foo';
310+
inner.displayName = 'Inner';
311+
const RefForwardingComponent = React.forwardRef(inner);
312+
313+
RefForwardingComponent.propTypes = {
314+
optional: PropTypes.string,
315+
required: PropTypes.string.isRequired,
316+
};
317+
318+
RefForwardingComponent.defaultProps = {
319+
optional: 'default',
320+
};
321+
322+
const ref = React.createRef();
323+
324+
expect(() =>
325+
ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />),
326+
).toErrorDev(
327+
'Warning: Failed prop type: The prop `required` is marked as required in ' +
328+
'`ForwardRef(Inner)`, but its value is `undefined`.\n' +
329+
' in Inner (at **)',
330+
);
331+
});
332+
333+
it('should honor a outer displayName when wrapped component and memo component set displayName at the same time.', () => {
334+
const Component = props => <div {...props} />;
335+
336+
const inner = (props, ref) => <Component {...props} forwardedRef={ref} />;
337+
inner.displayName = 'Inner';
254338
const RefForwardingComponent = React.forwardRef(inner);
339+
RefForwardingComponent.displayName = 'Outer';
255340

256341
RefForwardingComponent.propTypes = {
257342
optional: PropTypes.string,
@@ -268,8 +353,8 @@ describe('forwardRef', () => {
268353
ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />),
269354
).toErrorDev(
270355
'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 **)',
356+
'`Outer`, but its value is `undefined`.\n' +
357+
' in Inner (at **)',
273358
);
274359
});
275360

packages/shared/getComponentNameFromType.js

+10-5
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)