@samitouri / QOS-React / commits / d14ba87b1b

Validate propTypes for lazy() and memo() and warn about invalid patterns (#14298)

* Add a test for current defaultProps behavior in lazy * Add a warning against definining defaultProps on the outer wrapper * Warn about setting propTypes too * Remove redundant async * Validate propTypes for resolved lazy types Note this only works for elements created after resolving. So it's not ideal. But it provides the best stack trace for those cases. * Add a test for lazy(forwardRef()) propTypes check * Validate memo() inner propTypes and warn about shadowing * Add test verifying nested lazy is unsupported * Change error wording to remove "Promise elements" * Improve error message for nested lazy() and add tests * Validate propTypes for memo in the reconciler when necessary * Add comments for why we're calling checkPropTypes * Fix Flow and lint * Undo unintentional formatting changes * Remove unnecessary case (it is handled by function code path) * Add test coverage for memo(fn.defaultProps).propTypes * Test should be agnostic of where resolving happens That's an implementation detail and we might want to change it later. Let's keep it easy by making tests just check that validation happened, not at which stage. * Unify traversal logic in createElement This moves all type traversal into createElement. When lazy resolves, we call createElement once to re-check. * Match prod behavior for propTypes/defaultProps shims closer * Revert "Unify traversal logic in createElement" This reverts commit 2e77ca47fe80ebe6595333542a8c5c138c68643f. See https://github.com/facebook/react/pull/14298#issuecomment-442687775 * Undo unnecessary change to getComponentName

Dan Abramov committed Nov 29, 2018 at 20:06 UTC d14ba87b1bfed76900d6d25722f069003561e2e3
5 files changed +654 -12
packages/react-reconciler/src/ReactFiberBeginWork.js
+146 -1
@@ -54,6 +54,8 @@ import invariant from 'shared/invariant';
54 import shallowEqual from 'shared/shallowEqual';
55 import getComponentName from 'shared/getComponentName';
56 import ReactStrictModeWarnings from './ReactStrictModeWarnings';
57 +import {refineResolvedLazyComponent} from 'shared/ReactLazyComponent';
58 +import {REACT_LAZY_TYPE} from 'shared/ReactSymbols';
59 import warning from 'shared/warning';
60 import warningWithoutStack from 'shared/warningWithoutStack';
61 import {
@@ -212,6 +214,23 @@ function updateForwardRef(
214 nextProps: any,
215 renderExpirationTime: ExpirationTime,
216 ) {
217 + if (__DEV__) {
218 + if (workInProgress.type !== workInProgress.elementType) {
219 + // Lazy component props can't be validated in createElement
220 + // because they're only guaranteed to be resolved here.
221 + const innerPropTypes = Component.propTypes;
222 + if (innerPropTypes) {
223 + checkPropTypes(
224 + innerPropTypes,
225 + nextProps, // Resolved props
226 + 'prop',
227 + getComponentName(Component),
228 + getCurrentFiberStackInDev,
229 + );
230 + }
231 + }
232 + }
233 +
234 const render = Component.render;
235 const ref = workInProgress.ref;
236
@@ -273,6 +292,20 @@ function updateMemoComponent(
292 renderExpirationTime,
293 );
294 }
295 + if (__DEV__) {
296 + const innerPropTypes = type.propTypes;
297 + if (innerPropTypes) {
298 + // Inner memo component props aren't currently validated in createElement.
299 + // We could move it there, but we'd still need this for lazy code path.
300 + checkPropTypes(
301 + innerPropTypes,
302 + nextProps, // Resolved props
303 + 'prop',
304 + getComponentName(type),
305 + getCurrentFiberStackInDev,
306 + );
307 + }
308 + }
309 let child = createFiberFromTypeAndProps(
310 Component.type,
311 null,
@@ -286,6 +319,21 @@ function updateMemoComponent(
319 workInProgress.child = child;
320 return child;
321 }
322 + if (__DEV__) {
323 + const type = Component.type;
324 + const innerPropTypes = type.propTypes;
325 + if (innerPropTypes) {
326 + // Inner memo component props aren't currently validated in createElement.
327 + // We could move it there, but we'd still need this for lazy code path.
328 + checkPropTypes(
329 + innerPropTypes,
330 + nextProps, // Resolved props
331 + 'prop',
332 + getComponentName(type),
333 + getCurrentFiberStackInDev,
334 + );
335 + }
336 + }
337 let currentChild = ((current.child: any): Fiber); // This is always exactly one child
338 if (updateExpirationTime < renderExpirationTime) {
339 // This will be the props with resolved defaultProps,
@@ -323,6 +371,30 @@ function updateSimpleMemoComponent(
371 updateExpirationTime,
372 renderExpirationTime: ExpirationTime,
373 ): null | Fiber {
374 + if (__DEV__) {
375 + if (workInProgress.type !== workInProgress.elementType) {
376 + // Lazy component props can't be validated in createElement
377 + // because they're only guaranteed to be resolved here.
378 + let outerMemoType = workInProgress.elementType;
379 + if (outerMemoType.$$typeof === REACT_LAZY_TYPE) {
380 + // We warn when you define propTypes on lazy()
381 + // so let's just skip over it to find memo() outer wrapper.
382 + // Inner props for memo are validated later.
383 + outerMemoType = refineResolvedLazyComponent(outerMemoType);
384 + }
385 + const outerPropTypes = outerMemoType && (outerMemoType: any).propTypes;
386 + if (outerPropTypes) {
387 + checkPropTypes(
388 + outerPropTypes,
389 + nextProps, // Resolved (SimpleMemoComponent has no defaultProps)
390 + 'prop',
391 + getComponentName(outerMemoType),
392 + getCurrentFiberStackInDev,
393 + );
394 + }
395 + // Inner propTypes will be validated in the function component path.
396 + }
397 + }
398 if (current !== null && updateExpirationTime < renderExpirationTime) {
399 const prevProps = current.memoizedProps;
400 if (
@@ -412,6 +484,23 @@ function updateFunctionComponent(
484 nextProps: any,
485 renderExpirationTime,
486 ) {
487 + if (__DEV__) {
488 + if (workInProgress.type !== workInProgress.elementType) {
489 + // Lazy component props can't be validated in createElement
490 + // because they're only guaranteed to be resolved here.
491 + const innerPropTypes = Component.propTypes;
492 + if (innerPropTypes) {
493 + checkPropTypes(
494 + innerPropTypes,
495 + nextProps, // Resolved props
496 + 'prop',
497 + getComponentName(Component),
498 + getCurrentFiberStackInDev,
499 + );
500 + }
501 + }
502 + }
503 +
504 const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
505 const context = getMaskedContext(workInProgress, unmaskedContext);
506
@@ -446,6 +535,23 @@ function updateClassComponent(
535 nextProps,
536 renderExpirationTime: ExpirationTime,
537 ) {
538 + if (__DEV__) {
539 + if (workInProgress.type !== workInProgress.elementType) {
540 + // Lazy component props can't be validated in createElement
541 + // because they're only guaranteed to be resolved here.
542 + const innerPropTypes = Component.propTypes;
543 + if (innerPropTypes) {
544 + checkPropTypes(
545 + innerPropTypes,
546 + nextProps, // Resolved props
547 + 'prop',
548 + getComponentName(Component),
549 + getCurrentFiberStackInDev,
550 + );
551 + }
552 + }
553 + }
554 +
555 // Push context providers early to prevent context stack mismatches.
556 // During mounting we don't know the child context yet as the instance doesn't exist.
557 // We will invalidate the child context in finishClassComponent() right after rendering.
@@ -827,6 +933,20 @@ function mountLazyComponent(
933 break;
934 }
935 case MemoComponent: {
936 + if (__DEV__) {
937 + if (workInProgress.type !== workInProgress.elementType) {
938 + const outerPropTypes = Component.propTypes;
939 + if (outerPropTypes) {
940 + checkPropTypes(
941 + outerPropTypes,
942 + resolvedProps, // Resolved for outer only
943 + 'prop',
944 + getComponentName(Component),
945 + getCurrentFiberStackInDev,
946 + );
947 + }
948 + }
949 + }
950 child = updateMemoComponent(
951 null,
952 workInProgress,
@@ -838,14 +958,25 @@ function mountLazyComponent(
958 break;
959 }
960 default: {
961 + let hint = '';
962 + if (__DEV__) {
963 + if (
964 + Component !== null &&
965 + typeof Component === 'object' &&
966 + Component.$$typeof === REACT_LAZY_TYPE
967 + ) {
968 + hint = ' Did you wrap a component in React.lazy() more than once?';
969 + }
970 + }
971 // This message intentionally doesn't mention ForwardRef or MemoComponent
972 // because the fact that it's a separate type of work is an
973 // implementation detail.
974 invariant(
975 false,
976 'Element type is invalid. Received a promise that resolves to: %s. ' +
847 - 'Promise elements must resolve to a class or function.',
977 + 'Lazy element type must resolve to a class or function.%s',
978 Component,
979 + hint,
980 );
981 }
982 }
@@ -1746,6 +1877,20 @@ function beginWork(
1877 const unresolvedProps = workInProgress.pendingProps;
1878 // Resolve outer props first, then resolve inner props.
1879 let resolvedProps = resolveDefaultProps(type, unresolvedProps);
1880 + if (__DEV__) {
1881 + if (workInProgress.type !== workInProgress.elementType) {
1882 + const outerPropTypes = type.propTypes;
1883 + if (outerPropTypes) {
1884 + checkPropTypes(
1885 + outerPropTypes,
1886 + resolvedProps, // Resolved for outer only
1887 + 'prop',
1888 + getComponentName(type),
1889 + getCurrentFiberStackInDev,
1890 + );
1891 + }
1892 + }
1893 + }
1894 resolvedProps = resolveDefaultProps(type.type, resolvedProps);
1895 return updateMemoComponent(
1896 current,
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+356
@@ -1,3 +1,4 @@
1 +let PropTypes;
2 let React;
3 let ReactTestRenderer;
4 let ReactFeatureFlags;
@@ -10,6 +11,7 @@ describe('ReactLazy', () => {
11 ReactFeatureFlags = require('shared/ReactFeatureFlags');
12 ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
13 ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
14 + PropTypes = require('prop-types');
15 React = require('react');
16 Suspense = React.Suspense;
17 lazy = React.lazy;
@@ -461,6 +463,360 @@ describe('ReactLazy', () => {
463 expect(root).toMatchRenderedOutput('A3');
464 });
465
466 + it('resolves defaultProps on the outer wrapper but warns', async () => {
467 + function T(props) {
468 + ReactTestRenderer.unstable_yield(props.inner + ' ' + props.outer);
469 + return props.inner + ' ' + props.outer;
470 + }
471 + T.defaultProps = {inner: 'Hi'};
472 + const LazyText = lazy(() => fakeImport(T));
473 + expect(() => {
474 + LazyText.defaultProps = {outer: 'Bye'};
475 + }).toWarnDev(
476 + 'React.lazy(...): It is not supported to assign `defaultProps` to ' +
477 + 'a lazy component import. Either specify them where the component ' +
478 + 'is defined, or create a wrapping component around it.',
479 + {withoutStack: true},
480 + );
481 +
482 + const root = ReactTestRenderer.create(
483 + <Suspense fallback={<Text text="Loading..." />}>
484 + <LazyText />
485 + </Suspense>,
486 + {
487 + unstable_isConcurrent: true,
488 + },
489 + );
490 +
491 + expect(root).toFlushAndYield(['Loading...']);
492 + expect(root).toMatchRenderedOutput(null);
493 +
494 + await Promise.resolve();
495 + expect(root).toFlushAndYield(['Hi Bye']);
496 + expect(root).toMatchRenderedOutput('Hi Bye');
497 +
498 + root.update(
499 + <Suspense fallback={<Text text="Loading..." />}>
500 + <LazyText outer="World" />
501 + </Suspense>,
502 + );
503 + expect(root).toFlushAndYield(['Hi World']);
504 + expect(root).toMatchRenderedOutput('Hi World');
505 +
506 + root.update(
507 + <Suspense fallback={<Text text="Loading..." />}>
508 + <LazyText inner="Friends" />
509 + </Suspense>,
510 + );
511 + expect(root).toFlushAndYield(['Friends Bye']);
512 + expect(root).toMatchRenderedOutput('Friends Bye');
513 + });
514 +
515 + it('throws with a useful error when wrapping invalid type with lazy()', async () => {
516 + const BadLazy = lazy(() => fakeImport(42));
517 +
518 + const root = ReactTestRenderer.create(
519 + <Suspense fallback={<Text text="Loading..." />}>
520 + <BadLazy />
521 + </Suspense>,
522 + {
523 + unstable_isConcurrent: true,
524 + },
525 + );
526 +
527 + expect(root).toFlushAndYield(['Loading...']);
528 + expect(root).toMatchRenderedOutput(null);
529 +
530 + await Promise.resolve();
531 + root.update(
532 + <Suspense fallback={<Text text="Loading..." />}>
533 + <BadLazy />
534 + </Suspense>,
535 + );
536 + expect(() => {
537 + root.unstable_flushAll();
538 + }).toThrow(
539 + 'Element type is invalid. Received a promise that resolves to: 42. ' +
540 + 'Lazy element type must resolve to a class or function.',
541 + );
542 + });
543 +
544 + it('throws with a useful error when wrapping lazy() multiple times', async () => {
545 + const Lazy1 = lazy(() => fakeImport(Text));
546 + const Lazy2 = lazy(() => fakeImport(Lazy1));
547 +
548 + const root = ReactTestRenderer.create(
549 + <Suspense fallback={<Text text="Loading..." />}>
550 + <Lazy2 text="Hello" />
551 + </Suspense>,
552 + {
553 + unstable_isConcurrent: true,
554 + },
555 + );
556 +
557 + expect(root).toFlushAndYield(['Loading...']);
558 + expect(root).toMatchRenderedOutput(null);
559 +
560 + await Promise.resolve();
561 + root.update(
562 + <Suspense fallback={<Text text="Loading..." />}>
563 + <Lazy2 text="Hello" />
564 + </Suspense>,
565 + );
566 + expect(() => {
567 + root.unstable_flushAll();
568 + }).toThrow(
569 + 'Element type is invalid. Received a promise that resolves to: [object Object]. ' +
570 + 'Lazy element type must resolve to a class or function.' +
571 + (__DEV__
572 + ? ' Did you wrap a component in React.lazy() more than once?'
573 + : ''),
574 + );
575 + });
576 +
577 + it('warns about defining propTypes on the outer wrapper', () => {
578 + const LazyText = lazy(() => fakeImport(Text));
579 + expect(() => {
580 + LazyText.propTypes = {hello: () => {}};
581 + }).toWarnDev(
582 + 'React.lazy(...): It is not supported to assign `propTypes` to ' +
583 + 'a lazy component import. Either specify them where the component ' +
584 + 'is defined, or create a wrapping component around it.',
585 + {withoutStack: true},
586 + );
587 + });
588 +
589 + async function verifyInnerPropTypesAreChecked(Add) {
590 + const LazyAdd = lazy(() => fakeImport(Add));
591 + expect(() => {
592 + LazyAdd.propTypes = {};
593 + }).toWarnDev(
594 + 'React.lazy(...): It is not supported to assign `propTypes` to ' +
595 + 'a lazy component import. Either specify them where the component ' +
596 + 'is defined, or create a wrapping component around it.',
597 + {withoutStack: true},
598 + );
599 +
600 + const root = ReactTestRenderer.create(
601 + <Suspense fallback={<Text text="Loading..." />}>
602 + <LazyAdd inner="2" outer="2" />
603 + </Suspense>,
604 + {
605 + unstable_isConcurrent: true,
606 + },
607 + );
608 +
609 + expect(root).toFlushAndYield(['Loading...']);
610 + expect(root).toMatchRenderedOutput(null);
611 +
612 + // Mount
613 + await Promise.resolve();
614 + expect(() => {
615 + root.unstable_flushAll();
616 + }).toWarnDev(
617 + 'Invalid prop `inner` of type `string` supplied to `Add`, expected `number`.',
618 + );
619 + expect(root).toMatchRenderedOutput('22');
620 +
621 + // Update
622 + expect(() => {
623 + root.update(
624 + <Suspense fallback={<Text text="Loading..." />}>
625 + <LazyAdd inner={false} outer={false} />
626 + </Suspense>,
627 + );
628 + root.unstable_flushAll();
629 + }).toWarnDev(
630 + 'Invalid prop `inner` of type `boolean` supplied to `Add`, expected `number`.',
631 + );
632 + expect(root).toMatchRenderedOutput('0');
633 + }
634 +
635 + // Note: all "with defaultProps" tests below also verify defaultProps works as expected.
636 + // If we ever delete or move propTypes-related tests, make sure not to delete these.
637 + it('respects propTypes on function component with defaultProps', async () => {
638 + function Add(props) {
639 + expect(props.innerWithDefault).toBe(42);
640 + return props.inner + props.outer;
641 + }
642 + Add.propTypes = {
643 + inner: PropTypes.number.isRequired,
644 + innerWithDefault: PropTypes.number.isRequired,
645 + };
646 + Add.defaultProps = {
647 + innerWithDefault: 42,
648 + };
649 + await verifyInnerPropTypesAreChecked(Add);
650 + });
651 +
652 + it('respects propTypes on function component without defaultProps', async () => {
653 + function Add(props) {
654 + return props.inner + props.outer;
655 + }
656 + Add.propTypes = {
657 + inner: PropTypes.number.isRequired,
658 + };
659 + await verifyInnerPropTypesAreChecked(Add);
660 + });
661 +
662 + it('respects propTypes on class component with defaultProps', async () => {
663 + class Add extends React.Component {
664 + render() {
665 + expect(this.props.innerWithDefault).toBe(42);
666 + return this.props.inner + this.props.outer;
667 + }
668 + }
669 + Add.propTypes = {
670 + inner: PropTypes.number.isRequired,
671 + innerWithDefault: PropTypes.number.isRequired,
672 + };
673 + Add.defaultProps = {
674 + innerWithDefault: 42,
675 + };
676 + await verifyInnerPropTypesAreChecked(Add);
677 + });
678 +
679 + it('respects propTypes on class component without defaultProps', async () => {
680 + class Add extends React.Component {
681 + render() {
682 + return this.props.inner + this.props.outer;
683 + }
684 + }
685 + Add.propTypes = {
686 + inner: PropTypes.number.isRequired,
687 + };
688 + await verifyInnerPropTypesAreChecked(Add);
689 + });
690 +
691 + it('respects propTypes on forwardRef component with defaultProps', async () => {
692 + const Add = React.forwardRef((props, ref) => {
693 + expect(props.innerWithDefault).toBe(42);
694 + return props.inner + props.outer;
695 + });
696 + Add.displayName = 'Add';
697 + Add.propTypes = {
698 + inner: PropTypes.number.isRequired,
699 + innerWithDefault: PropTypes.number.isRequired,
700 + };
701 + Add.defaultProps = {
702 + innerWithDefault: 42,
703 + };
704 + await verifyInnerPropTypesAreChecked(Add);
705 + });
706 +
707 + it('respects propTypes on forwardRef component without defaultProps', async () => {
708 + const Add = React.forwardRef((props, ref) => {
709 + return props.inner + props.outer;
710 + });
711 + Add.displayName = 'Add';
712 + Add.propTypes = {
713 + inner: PropTypes.number.isRequired,
714 + };
715 + await verifyInnerPropTypesAreChecked(Add);
716 + });
717 +
718 + it('respects propTypes on outer memo component with defaultProps', async () => {
719 + let Add = props => {
720 + expect(props.innerWithDefault).toBe(42);
721 + return props.inner + props.outer;
722 + };
723 + Add = React.memo(Add);
724 + Add.propTypes = {
725 + inner: PropTypes.number.isRequired,
726 + innerWithDefault: PropTypes.number.isRequired,
727 + };
728 + Add.defaultProps = {
729 + innerWithDefault: 42,
730 + };
731 + await verifyInnerPropTypesAreChecked(Add);
732 + });
733 +
734 + it('respects propTypes on outer memo component without defaultProps', async () => {
735 + let Add = props => {
736 + return props.inner + props.outer;
737 + };
738 + Add = React.memo(Add);
739 + Add.propTypes = {
740 + inner: PropTypes.number.isRequired,
741 + };
742 + await verifyInnerPropTypesAreChecked(Add);
743 + });
744 +
745 + it('respects propTypes on inner memo component with defaultProps', async () => {
746 + const Add = props => {
747 + expect(props.innerWithDefault).toBe(42);
748 + return props.inner + props.outer;
749 + };
750 + Add.displayName = 'Add';
751 + Add.propTypes = {
752 + inner: PropTypes.number.isRequired,
753 + innerWithDefault: PropTypes.number.isRequired,
754 + };
755 + Add.defaultProps = {
756 + innerWithDefault: 42,
757 + };
758 + await verifyInnerPropTypesAreChecked(React.memo(Add));
759 + });
760 +
761 + it('respects propTypes on inner memo component without defaultProps', async () => {
762 + const Add = props => {
763 + return props.inner + props.outer;
764 + };
765 + Add.displayName = 'Add';
766 + Add.propTypes = {
767 + inner: PropTypes.number.isRequired,
768 + };
769 + await verifyInnerPropTypesAreChecked(React.memo(Add));
770 + });
771 +
772 + it('uses outer resolved props for validating propTypes on memo', async () => {
773 + let T = props => {
774 + return <Text text={props.text} />;
775 + };
776 + T.defaultProps = {
777 + text: 'Inner default text',
778 + };
779 + T = React.memo(T);
780 + T.propTypes = {
781 + // Should not be satisfied by the *inner* defaultProps.
782 + text: PropTypes.string.isRequired,
783 + };
784 + const LazyText = lazy(() => fakeImport(T));
785 + const root = ReactTestRenderer.create(
786 + <Suspense fallback={<Text text="Loading..." />}>
787 + <LazyText />
788 + </Suspense>,
789 + {
790 + unstable_isConcurrent: true,
791 + },
792 + );
793 +
794 + expect(root).toFlushAndYield(['Loading...']);
795 + expect(root).toMatchRenderedOutput(null);
796 +
797 + // Mount
798 + await Promise.resolve();
799 + expect(() => {
800 + root.unstable_flushAll();
801 + }).toWarnDev(
802 + 'The prop `text` is marked as required in `T`, but its value is `undefined`',
803 + );
804 + expect(root).toMatchRenderedOutput('Inner default text');
805 +
806 + // Update
807 + expect(() => {
808 + root.update(
809 + <Suspense fallback={<Text text="Loading..." />}>
810 + <LazyText text={null} />
811 + </Suspense>,
812 + );
813 + root.unstable_flushAll();
814 + }).toWarnDev(
815 + 'The prop `text` is marked as required in `T`, but its value is `null`',
816 + );
817 + expect(root).toMatchRenderedOutput(null);
818 + });
819 +
820 it('includes lazy-loaded component in warning stack', async () => {
821 const LazyFoo = lazy(() => {
822 ReactTestRenderer.unstable_yield('Started loading');
packages/react-reconciler/src/__tests__/ReactMemo-test.internal.js
+92
@@ -12,6 +12,7 @@
12
13 'use strict';
14
15 +let PropTypes;
16 let React;
17 let ReactFeatureFlags;
18 let ReactNoop;
@@ -22,6 +23,7 @@ describe('memo', () => {
23 jest.resetModules();
24 ReactFeatureFlags = require('shared/ReactFeatureFlags');
25 ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
26 + PropTypes = require('prop-types');
27 React = require('react');
28 ReactNoop = require('react-noop-renderer');
29 ({Suspense} = React);
@@ -321,6 +323,96 @@ describe('memo', () => {
323 {withoutStack: true},
324 );
325 });
326 +
327 + it('validates propTypes declared on the inner component', () => {
328 + function FnInner(props) {
329 + return props.inner;
330 + }
331 + FnInner.propTypes = {inner: PropTypes.number.isRequired};
332 + const Fn = React.memo(FnInner);
333 +
334 + // Mount
335 + expect(() => {
336 + ReactNoop.render(<Fn inner="2" />);
337 + ReactNoop.flush();
338 + }).toWarnDev(
339 + 'Invalid prop `inner` of type `string` supplied to `FnInner`, expected `number`.',
340 + );
341 +
342 + // Update
343 + expect(() => {
344 + ReactNoop.render(<Fn inner={false} />);
345 + ReactNoop.flush();
346 + }).toWarnDev(
347 + 'Invalid prop `inner` of type `boolean` supplied to `FnInner`, expected `number`.',
348 + );
349 + });
350 +
351 + it('validates propTypes declared on the outer component', () => {
352 + function FnInner(props) {
353 + return props.outer;
354 + }
355 + const Fn = React.memo(FnInner);
356 + Fn.propTypes = {outer: PropTypes.number.isRequired};
357 +
358 + // Mount
359 + expect(() => {
360 + ReactNoop.render(<Fn outer="3" />);
361 + ReactNoop.flush();
362 + }).toWarnDev(
363 + // Outer props are checked in createElement
364 + 'Invalid prop `outer` of type `string` supplied to `FnInner`, expected `number`.',
365 + );
366 +
367 + // Update
368 + expect(() => {
369 + ReactNoop.render(<Fn outer={false} />);
370 + ReactNoop.flush();
371 + }).toWarnDev(
372 + // Outer props are checked in createElement
373 + 'Invalid prop `outer` of type `boolean` supplied to `FnInner`, expected `number`.',
374 + );
375 + });
376 +
377 + it('validates nested propTypes declarations', () => {
378 + function Inner(props) {
379 + return props.inner + props.middle + props.outer;
380 + }
381 + Inner.propTypes = {inner: PropTypes.number.isRequired};
382 + Inner.defaultProps = {inner: 0};
383 + const Middle = React.memo(Inner);
384 + Middle.propTypes = {middle: PropTypes.number.isRequired};
385 + Middle.defaultProps = {middle: 0};
386 + const Outer = React.memo(Middle);
387 + Outer.propTypes = {outer: PropTypes.number.isRequired};
388 + Outer.defaultProps = {outer: 0};
389 +
390 + // No warning expected because defaultProps satisfy both.
391 + ReactNoop.render(<Outer />);
392 + ReactNoop.flush();
393 +
394 + // Mount
395 + expect(() => {
396 + ReactNoop.render(<Outer inner="2" middle="3" outer="4" />);
397 + ReactNoop.flush();
398 + }).toWarnDev([
399 + 'Invalid prop `outer` of type `string` supplied to `Inner`, expected `number`.',
400 + 'Invalid prop `middle` of type `string` supplied to `Inner`, expected `number`.',
401 + 'Invalid prop `inner` of type `string` supplied to `Inner`, expected `number`.',
402 + ]);
403 +
404 + // Update
405 + expect(() => {
406 + ReactNoop.render(
407 + <Outer inner={false} middle={false} outer={false} />,
408 + );
409 + ReactNoop.flush();
410 + }).toWarnDev([
411 + 'Invalid prop `outer` of type `boolean` supplied to `Inner`, expected `number`.',
412 + 'Invalid prop `middle` of type `boolean` supplied to `Inner`, expected `number`.',
413 + 'Invalid prop `inner` of type `boolean` supplied to `Inner`, expected `number`.',
414 + ]);
415 + });
416 });
417 }
418 });
packages/react/src/ReactElementValidator.js
+10 -10
@@ -18,6 +18,7 @@ import getComponentName from 'shared/getComponentName';
18 import {
19 getIteratorFn,
20 REACT_FORWARD_REF_TYPE,
21 + REACT_MEMO_TYPE,
22 REACT_FRAGMENT_TYPE,
23 REACT_ELEMENT_TYPE,
24 } from 'shared/ReactSymbols';
@@ -185,21 +186,20 @@ function validateChildKeys(node, parentType) {
186 */
187 function validatePropTypes(element) {
188 const type = element.type;
188 - let name, propTypes;
189 + if (type === null || type === undefined || typeof type === 'string') {
190 + return;
191 + }
192 + const name = getComponentName(type);
193 + let propTypes;
194 if (typeof type === 'function') {
190 - // Class or function component
191 - name = type.displayName || type.name;
195 propTypes = type.propTypes;
196 } else if (
197 typeof type === 'object' &&
195 - type !== null &&
196 - type.$$typeof === REACT_FORWARD_REF_TYPE
198 + (type.$$typeof === REACT_FORWARD_REF_TYPE ||
199 + // Note: Memo only checks outer props here.
200 + // Inner props are checked in the reconciler.
201 + type.$$typeof === REACT_MEMO_TYPE)
202 ) {
198 - // ForwardRef
199 - const functionName = type.render.displayName || type.render.name || '';
200 - name =
201 - type.displayName ||
202 - (functionName !== '' ? `ForwardRef(${functionName})` : 'ForwardRef');
203 propTypes = type.propTypes;
204 } else {
205 return;
packages/react/src/ReactLazy.js
+50 -1
@@ -8,13 +8,62 @@
8 import type {LazyComponent, Thenable} from 'shared/ReactLazyComponent';
9
10 import {REACT_LAZY_TYPE} from 'shared/ReactSymbols';
11 +import warning from 'shared/warning';
12
13 export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {
13 - return {
14 + let lazyType = {
15 $$typeof: REACT_LAZY_TYPE,
16 _ctor: ctor,
17 // React uses these fields to store the result.
18 _status: -1,
19 _result: null,
20 };
21 +
22 + if (__DEV__) {
23 + // In production, this would just set it on the object.
24 + let defaultProps;
25 + let propTypes;
26 + Object.defineProperties(lazyType, {
27 + defaultProps: {
28 + configurable: true,
29 + get() {
30 + return defaultProps;
31 + },
32 + set(newDefaultProps) {
33 + warning(
34 + false,
35 + 'React.lazy(...): It is not supported to assign `defaultProps` to ' +
36 + 'a lazy component import. Either specify them where the component ' +
37 + 'is defined, or create a wrapping component around it.',
38 + );
39 + defaultProps = newDefaultProps;
40 + // Match production behavior more closely:
41 + Object.defineProperty(lazyType, 'defaultProps', {
42 + enumerable: true,
43 + });
44 + },
45 + },
46 + propTypes: {
47 + configurable: true,
48 + get() {
49 + return propTypes;
50 + },
51 + set(newPropTypes) {
52 + warning(
53 + false,
54 + 'React.lazy(...): It is not supported to assign `propTypes` to ' +
55 + 'a lazy component import. Either specify them where the component ' +
56 + 'is defined, or create a wrapping component around it.',
57 + );
58 + propTypes = newPropTypes;
59 + // Match production behavior more closely:
60 + Object.defineProperty(lazyType, 'propTypes', {
61 + enumerable: true,
62 + });
63 + },
64 + },
65 + });
66 + }
67 +
68 + return lazyType;
69 }