Unify React.memo and React.forwardRef display name logic (#21392)
Co-authored-by: iChenLei <2470828450@qq.com>
Brian Vaughn committed
May 4, 2021 at 11:40 UTC
d1542de3a607024421e13db01a42d8ba5930c389
8 files changed
+220
-42
packages/react-devtools-shared/src/__tests__/__snapshots__/store-test.js.snap
-17
@@ -670,20 +670,3 @@ exports[`Store should properly serialize non-string key values: 1: mount 1`] = `
670
[root]
671
<Child key="123">
672
`;
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
@@ -879,6 +879,17 @@ describe('Store', () => {
879
FakeHigherOrderComponent,
880
);
881
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
+
893
const App = () => (
894
<React.Fragment>
895
<MyComponent />
@@ -891,6 +902,8 @@ describe('Store', () => {
902
<MemoizedFakeHigherOrderComponent />
903
<ForwardRefFakeHigherOrderComponent />
904
<React.unstable_Cache />
905
+ <MemoizedFakeHigherOrderComponentWithDisplayNameOverride />
906
+ <ForwardRefFakeHigherOrderComponentWithDisplayNameOverride />
907
</React.Fragment>
908
);
909
@@ -904,7 +917,24 @@ describe('Store', () => {
917
// Render again after it resolves
918
act(() => ReactDOM.render(<App />, container));
919
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
+ `);
938
});
939
940
describe('Lazy', () => {
packages/react-devtools-shared/src/backend/renderer.js
+6
-2
@@ -393,7 +393,7 @@ export function getInternalReactConstants(
393
394
// NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods
395
function getDisplayNameForFiber(fiber: Fiber): string | null {
396
- const {type, tag} = fiber;
396
+ const {elementType, type, tag} = fiber;
397
398
let resolvedType = type;
399
if (typeof type === 'object' && type !== null) {
@@ -432,7 +432,11 @@ export function getInternalReactConstants(
432
return 'Lazy';
433
case MemoComponent:
434
case SimpleMemoComponent:
435
- return getDisplayName(resolvedType, 'Anonymous');
435
+ return (
436
+ (elementType && elementType.displayName) ||
437
+ (type && type.displayName) ||
438
+ getDisplayName(resolvedType, 'Anonymous')
439
+ );
440
case SuspenseComponent:
441
return 'Suspense';
442
case LegacyHiddenComponent:
packages/react-reconciler/src/__tests__/ReactMemo-test.js
+63
-8
@@ -498,11 +498,66 @@ describe('memo', () => {
498
});
499
});
500
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
+
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
});
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';
561
MemoComponent.propTypes = {
562
required: PropTypes.string.isRequired,
563
};
@@ -511,19 +566,19 @@ describe('memo', () => {
566
ReactNoop.render(<MemoComponent optional="foo" />),
567
).toErrorDev(
568
'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 **)',
571
);
572
});
573
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.', () => {
575
function Component(props) {
576
return <div {...props} />;
577
}
523
- Component.displayName = 'Foo';
578
+ Component.displayName = 'Inner';
579
580
const MemoComponent = React.memo(Component);
526
- MemoComponent.displayName = 'Bar';
581
+ MemoComponent.displayName = 'Outer';
582
MemoComponent.propTypes = {
583
required: PropTypes.string.isRequired,
584
};
@@ -532,8 +587,8 @@ describe('memo', () => {
587
ReactNoop.render(<MemoComponent optional="foo" />),
588
).toErrorDev(
589
'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 **)',
592
);
593
});
594
}
packages/react/src/ReactForwardRef.js
+9
-1
@@ -57,7 +57,15 @@ export function forwardRef<Props, ElementType: React$ElementType>(
57
},
58
set: function(name) {
59
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) {
69
render.displayName = name;
70
}
71
},
packages/react/src/ReactMemo.js
+9
-1
@@ -37,7 +37,15 @@ export function memo<Props>(
37
},
38
set: function(name) {
39
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) {
49
type.displayName = name;
50
}
51
},
packages/react/src/__tests__/forwardRef-test.js
+92
-7
@@ -217,14 +217,43 @@ describe('forwardRef', () => {
217
);
218
});
219
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', () => {
221
const Component = props => <div {...props} />;
222
223
const RefForwardingComponent = React.forwardRef((props, ref) => (
224
<Component {...props} forwardedRef={ref} />
225
));
226
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';
257
258
RefForwardingComponent.propTypes = {
259
optional: PropTypes.string,
@@ -241,8 +270,36 @@ describe('forwardRef', () => {
270
ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />),
271
).toErrorDev(
272
'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 **)',
303
);
304
});
305
@@ -250,8 +307,36 @@ describe('forwardRef', () => {
307
const Component = props => <div {...props} />;
308
309
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';
338
const RefForwardingComponent = React.forwardRef(inner);
339
+ RefForwardingComponent.displayName = 'Outer';
340
341
RefForwardingComponent.propTypes = {
342
optional: PropTypes.string,
@@ -268,8 +353,8 @@ describe('forwardRef', () => {
353
ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />),
354
).toErrorDev(
355
'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 **)',
358
);
359
});
360
packages/shared/getComponentNameFromType.js
+10
-5
@@ -31,11 +31,12 @@ function getWrappedName(
31
innerType: any,
32
wrapperName: string,
33
): string {
34
+ const displayName = (outerType: any).displayName;
35
+ if (displayName) {
36
+ return displayName;
37
+ }
38
const functionName = innerType.displayName || innerType.name || '';
35
- return (
36
- (outerType: any).displayName ||
37
- (functionName !== '' ? `${wrapperName}(${functionName})` : wrapperName)
38
- );
39
+ return functionName !== '' ? `${wrapperName}(${functionName})` : wrapperName;
40
}
41
42
// Keep in sync with react-reconciler/getComponentNameFromFiber
@@ -90,7 +91,11 @@ export default function getComponentNameFromType(type: mixed): string | null {
91
case REACT_FORWARD_REF_TYPE:
92
return getWrappedName(type, type.render, 'ForwardRef');
93
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';
99
case REACT_LAZY_TYPE: {
100
const lazyComponent: LazyComponent<any, any> = (type: any);
101
const payload = lazyComponent._payload;