diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 0d87dbd81aaa6..95ac5e066e06b 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -2293,6 +2293,7 @@ describe('ReactHooksInspectionIntegration', () => { }); }); + // @gate !disableDefaultPropsExceptForClasses it('should support defaultProps and lazy', async () => { const Suspense = React.Suspense; diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 6064ad9312e87..47d00e6950694 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -405,18 +405,6 @@ describe('ReactDOMFizzServer', () => { } it('should asynchronously load a lazy component', async () => { - const originalConsoleError = console.error; - const mockError = jest.fn(); - console.error = (...args) => { - if (args.length > 1) { - if (typeof args[1] === 'object') { - mockError(args[0].split('\n')[0]); - return; - } - } - mockError(...args.map(normalizeCodeLocInfo)); - }; - let resolveA; const LazyA = React.lazy(() => { return new Promise(r => { @@ -431,74 +419,59 @@ describe('ReactDOMFizzServer', () => { }); }); - function TextWithPunctuation({text, punctuation}) { - return ; + class TextWithPunctuation extends React.Component { + render() { + return ; + } } + // This tests that default props of the inner element is resolved. TextWithPunctuation.defaultProps = { punctuation: '!', }; - try { - await act(() => { - const {pipe} = renderToPipeableStream( -
-
- }> - - -
-
- }> - - -
-
, - ); - pipe(writable); - }); - - expect(getVisibleChildren(container)).toEqual( -
-
Loading...
-
Loading...
-
, - ); - await act(() => { - resolveA({default: Text}); - }); - expect(getVisibleChildren(container)).toEqual( -
-
Hello
-
Loading...
-
, - ); - await act(() => { - resolveB({default: TextWithPunctuation}); - }); - expect(getVisibleChildren(container)).toEqual( + await act(() => { + const {pipe} = renderToPipeableStream(
-
Hello
-
world!
+
+ }> + + +
+
+ }> + + +
, ); + pipe(writable); + }); - if (__DEV__) { - expect(mockError).toHaveBeenCalledWith( - 'Warning: %s: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.%s', - 'TextWithPunctuation', - '\n in TextWithPunctuation (at **)\n' + - ' in Lazy (at **)\n' + - ' in Suspense (at **)\n' + - ' in div (at **)\n' + - ' in div (at **)', - ); - } else { - expect(mockError).not.toHaveBeenCalled(); - } - } finally { - console.error = originalConsoleError; - } + expect(getVisibleChildren(container)).toEqual( +
+
Loading...
+
Loading...
+
, + ); + await act(() => { + resolveA({default: Text}); + }); + expect(getVisibleChildren(container)).toEqual( +
+
Hello
+
Loading...
+
, + ); + await act(() => { + resolveB({default: TextWithPunctuation}); + }); + expect(getVisibleChildren(container)).toEqual( +
+
Hello
+
world!
+
, + ); }); it('#23331: does not warn about hydration mismatches if something suspended in an earlier sibling', async () => { diff --git a/packages/react-dom/src/__tests__/ReactDeprecationWarnings-test.js b/packages/react-dom/src/__tests__/ReactDeprecationWarnings-test.js index f2ec54f4ea018..a6ac2da8fe044 100644 --- a/packages/react-dom/src/__tests__/ReactDeprecationWarnings-test.js +++ b/packages/react-dom/src/__tests__/ReactDeprecationWarnings-test.js @@ -43,6 +43,7 @@ describe('ReactDeprecationWarnings', () => { ); }); + // @gate !disableDefaultPropsExceptForClasses it('should warn when given defaultProps on a memoized function', async () => { const MemoComponent = React.memo(function FunctionalComponent(props) { return null; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 8c7ba485856f2..e6d5d0ab3aefa 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -109,6 +109,7 @@ import { enableRenderableContext, enableRefAsProp, disableLegacyMode, + disableDefaultPropsExceptForClasses, } from 'shared/ReactFeatureFlags'; import isArray from 'shared/isArray'; import shallowEqual from 'shared/shallowEqual'; @@ -247,7 +248,7 @@ import { updateClassInstance, resolveClassComponentProps, } from './ReactFiberClassComponent'; -import {resolveDefaultProps} from './ReactFiberLazyComponent'; +import {resolveDefaultPropsOnNonClassComponent} from './ReactFiberLazyComponent'; import { createFiberFromTypeAndProps, createFiberFromFragment, @@ -488,7 +489,8 @@ function updateMemoComponent( isSimpleFunctionComponent(type) && Component.compare === null && // SimpleMemoComponent codepath doesn't resolve outer props either. - Component.defaultProps === undefined + (disableDefaultPropsExceptForClasses || + Component.defaultProps === undefined) ) { let resolvedType = type; if (__DEV__) { @@ -510,16 +512,18 @@ function updateMemoComponent( renderLanes, ); } - if (__DEV__) { - if (Component.defaultProps !== undefined) { - const componentName = getComponentNameFromType(type) || 'Unknown'; - if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) { - console.error( - '%s: Support for defaultProps will be removed from memo components ' + - 'in a future major release. Use JavaScript default parameters instead.', - componentName, - ); - didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true; + if (!disableDefaultPropsExceptForClasses) { + if (__DEV__) { + if (Component.defaultProps !== undefined) { + const componentName = getComponentNameFromType(type) || 'Unknown'; + if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) { + console.error( + '%s: Support for defaultProps will be removed from memo components ' + + 'in a future major release. Use JavaScript default parameters instead.', + componentName, + ); + didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true; + } } } } @@ -1779,7 +1783,9 @@ function mountLazyComponent( renderLanes, ); } else { - const resolvedProps = resolveDefaultProps(Component, props); + const resolvedProps = disableDefaultPropsExceptForClasses + ? props + : resolveDefaultPropsOnNonClassComponent(Component, props); workInProgress.tag = FunctionComponent; if (__DEV__) { validateFunctionComponentInDev(workInProgress, Component); @@ -1797,7 +1803,9 @@ function mountLazyComponent( } else if (Component !== undefined && Component !== null) { const $$typeof = Component.$$typeof; if ($$typeof === REACT_FORWARD_REF_TYPE) { - const resolvedProps = resolveDefaultProps(Component, props); + const resolvedProps = disableDefaultPropsExceptForClasses + ? props + : resolveDefaultPropsOnNonClassComponent(Component, props); workInProgress.tag = ForwardRef; if (__DEV__) { workInProgress.type = Component = @@ -1811,13 +1819,20 @@ function mountLazyComponent( renderLanes, ); } else if ($$typeof === REACT_MEMO_TYPE) { - const resolvedProps = resolveDefaultProps(Component, props); + const resolvedProps = disableDefaultPropsExceptForClasses + ? props + : resolveDefaultPropsOnNonClassComponent(Component, props); workInProgress.tag = MemoComponent; return updateMemoComponent( null, workInProgress, Component, - resolveDefaultProps(Component.type, resolvedProps), // The inner type can have defaults too + disableDefaultPropsExceptForClasses + ? resolvedProps + : resolveDefaultPropsOnNonClassComponent( + Component.type, + resolvedProps, + ), // The inner type can have defaults too renderLanes, ); } @@ -3928,9 +3943,10 @@ function beginWork( const Component = workInProgress.type; const unresolvedProps = workInProgress.pendingProps; const resolvedProps = + disableDefaultPropsExceptForClasses || workInProgress.elementType === Component ? unresolvedProps - : resolveDefaultProps(Component, unresolvedProps); + : resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps); return updateFunctionComponent( current, workInProgress, @@ -3979,9 +3995,10 @@ function beginWork( const type = workInProgress.type; const unresolvedProps = workInProgress.pendingProps; const resolvedProps = + disableDefaultPropsExceptForClasses || workInProgress.elementType === type ? unresolvedProps - : resolveDefaultProps(type, unresolvedProps); + : resolveDefaultPropsOnNonClassComponent(type, unresolvedProps); return updateForwardRef( current, workInProgress, @@ -4004,8 +4021,12 @@ function beginWork( const type = workInProgress.type; const unresolvedProps = workInProgress.pendingProps; // Resolve outer props first, then resolve inner props. - let resolvedProps = resolveDefaultProps(type, unresolvedProps); - resolvedProps = resolveDefaultProps(type.type, resolvedProps); + let resolvedProps = disableDefaultPropsExceptForClasses + ? unresolvedProps + : resolveDefaultPropsOnNonClassComponent(type, unresolvedProps); + resolvedProps = disableDefaultPropsExceptForClasses + ? resolvedProps + : resolveDefaultPropsOnNonClassComponent(type.type, resolvedProps); return updateMemoComponent( current, workInProgress, diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 697be25eee678..e464670bfedb4 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -24,6 +24,7 @@ import { enableSchedulingProfiler, enableLazyContextPropagation, enableRefAsProp, + disableDefaultPropsExceptForClasses, } from 'shared/ReactFeatureFlags'; import ReactStrictModeWarnings from './ReactStrictModeWarnings'; import {isMounted} from './ReactFiberTreeReflection'; @@ -1252,7 +1253,12 @@ export function resolveClassComponentProps( // Resolve default props. Taken from old JSX runtime, where this used to live. const defaultProps = Component.defaultProps; - if (defaultProps && !alreadyResolvedDefaultProps) { + if ( + defaultProps && + // If disableDefaultPropsExceptForClasses is true, we always resolve + // default props here in the reconciler, rather than in the JSX runtime. + (disableDefaultPropsExceptForClasses || !alreadyResolvedDefaultProps) + ) { newProps = assign({}, newProps, baseProps); for (const propName in defaultProps) { if (newProps[propName] === undefined) { diff --git a/packages/react-reconciler/src/ReactFiberLazyComponent.js b/packages/react-reconciler/src/ReactFiberLazyComponent.js index b6c28f0ed5cce..36e16c2b40800 100644 --- a/packages/react-reconciler/src/ReactFiberLazyComponent.js +++ b/packages/react-reconciler/src/ReactFiberLazyComponent.js @@ -8,12 +8,17 @@ */ import assign from 'shared/assign'; +import {disableDefaultPropsExceptForClasses} from 'shared/ReactFeatureFlags'; -export function resolveDefaultProps(Component: any, baseProps: Object): Object { - // TODO: Remove support for default props for everything except class - // components, including setting default props on a lazy wrapper around a - // class type. - +export function resolveDefaultPropsOnNonClassComponent( + Component: any, + baseProps: Object, +): Object { + if (disableDefaultPropsExceptForClasses) { + // Support for defaultProps is removed in React 19 for all types + // except classes. + return baseProps; + } if (Component && Component.defaultProps) { // Resolve default props. Taken from ReactElement const props = assign({}, baseProps); diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 36182d999403b..8a5ecdac70136 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -40,6 +40,7 @@ import { alwaysThrottleRetries, enableInfiniteRenderLoopDetection, disableLegacyMode, + disableDefaultPropsExceptForClasses, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import is from 'shared/objectIs'; @@ -264,7 +265,7 @@ import { getSuspenseHandler, getShellBoundary, } from './ReactFiberSuspenseContext'; -import {resolveDefaultProps} from './ReactFiberLazyComponent'; +import {resolveDefaultPropsOnNonClassComponent} from './ReactFiberLazyComponent'; import {resetChildReconcilerOnUnwind} from './ReactChildFiber'; import { ensureRootIsScheduled, @@ -2408,9 +2409,10 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void { const Component = unitOfWork.type; const unresolvedProps = unitOfWork.pendingProps; const resolvedProps = + disableDefaultPropsExceptForClasses || unitOfWork.elementType === Component ? unresolvedProps - : resolveDefaultProps(Component, unresolvedProps); + : resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps); let context: any; if (!disableLegacyContext) { const unmaskedContext = getUnmaskedContext(unitOfWork, Component, true); @@ -2434,9 +2436,10 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void { const Component = unitOfWork.type.render; const unresolvedProps = unitOfWork.pendingProps; const resolvedProps = + disableDefaultPropsExceptForClasses || unitOfWork.elementType === Component ? unresolvedProps - : resolveDefaultProps(Component, unresolvedProps); + : resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps); next = replayFunctionComponent( current, diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index 4091c064aac8b..03b9d62732b56 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -328,8 +328,10 @@ describe('ReactLazy', () => { }); it('resolves defaultProps, on mount and update', async () => { - function T(props) { - return ; + class T extends React.Component { + render() { + return ; + } } T.defaultProps = {text: 'Hi'}; const LazyText = lazy(() => fakeImport(T)); @@ -346,14 +348,8 @@ describe('ReactLazy', () => { await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('Hi'); - await expect(async () => { - await act(() => resolveFakeImport(T)); - assertLog(['Hi']); - }).toErrorDev( - 'Warning: T: Support for defaultProps ' + - 'will be removed from function components in a future major ' + - 'release. Use JavaScript default parameters instead.', - ); + await act(() => resolveFakeImport(T)); + assertLog(['Hi']); expect(root).toMatchRenderedOutput('Hi'); @@ -368,14 +364,16 @@ describe('ReactLazy', () => { }); it('resolves defaultProps without breaking memoization', async () => { - function LazyImpl(props) { - Scheduler.log('Lazy'); - return ( - <> - - {props.children} - - ); + class LazyImpl extends React.Component { + render() { + Scheduler.log('Lazy'); + return ( + <> + + {this.props.children} + + ); + } } LazyImpl.defaultProps = {siblingText: 'Sibling'}; const Lazy = lazy(() => fakeImport(LazyImpl)); @@ -402,14 +400,8 @@ describe('ReactLazy', () => { await waitForAll(['Loading...']); expect(root).not.toMatchRenderedOutput('SiblingA'); - await expect(async () => { - await act(() => resolveFakeImport(LazyImpl)); - assertLog(['Lazy', 'Sibling', 'A']); - }).toErrorDev( - 'Warning: LazyImpl: Support for defaultProps ' + - 'will be removed from function components in a future major ' + - 'release. Use JavaScript default parameters instead.', - ); + await act(() => resolveFakeImport(LazyImpl)); + assertLog(['Lazy', 'Sibling', 'A']); expect(root).toMatchRenderedOutput('SiblingA'); @@ -680,6 +672,7 @@ describe('ReactLazy', () => { expect(root).toMatchRenderedOutput('A3'); }); + // @gate !disableDefaultPropsExceptForClasses it('resolves defaultProps on the outer wrapper but warns', async () => { function T(props) { Scheduler.log(props.inner + ' ' + props.outer); @@ -837,6 +830,7 @@ describe('ReactLazy', () => { expect(root).toMatchRenderedOutput('0'); } + // @gate !disableDefaultPropsExceptForClasses it('resolves props for function component with defaultProps', async () => { function Add(props) { expect(props.innerWithDefault).toBe(42); @@ -877,6 +871,7 @@ describe('ReactLazy', () => { await verifyResolvesProps(Add); }); + // @gate !disableDefaultPropsExceptForClasses it('resolves props for forwardRef component with defaultProps', async () => { const Add = React.forwardRef((props, ref) => { expect(props.innerWithDefault).toBe(42); @@ -897,6 +892,7 @@ describe('ReactLazy', () => { await verifyResolvesProps(Add); }); + // @gate !disableDefaultPropsExceptForClasses it('resolves props for outer memo component with defaultProps', async () => { let Add = props => { expect(props.innerWithDefault).toBe(42); @@ -917,6 +913,7 @@ describe('ReactLazy', () => { await verifyResolvesProps(Add); }); + // @gate !disableDefaultPropsExceptForClasses it('resolves props for inner memo component with defaultProps', async () => { const Add = props => { expect(props.innerWithDefault).toBe(42); @@ -937,6 +934,7 @@ describe('ReactLazy', () => { await verifyResolvesProps(React.memo(Add)); }); + // @gate !disableDefaultPropsExceptForClasses it('uses outer resolved props on memo', async () => { let T = props => { return ; @@ -1052,6 +1050,7 @@ describe('ReactLazy', () => { }); // Regression test for #14310 + // @gate !disableDefaultPropsExceptForClasses it('supports defaultProps defined on the memo() return value', async () => { const Add = React.memo(props => { return props.inner + props.outer; @@ -1134,6 +1133,7 @@ describe('ReactLazy', () => { expect(root).toMatchRenderedOutput('3'); }); + // @gate !disableDefaultPropsExceptForClasses it('merges defaultProps in the correct order', async () => { let Add = React.memo(props => { return props.inner + props.outer; diff --git a/packages/react-reconciler/src/__tests__/ReactMemo-test.js b/packages/react-reconciler/src/__tests__/ReactMemo-test.js index a70d5aa53dd51..e8d8ef384aa44 100644 --- a/packages/react-reconciler/src/__tests__/ReactMemo-test.js +++ b/packages/react-reconciler/src/__tests__/ReactMemo-test.js @@ -65,19 +65,17 @@ describe('memo', () => { // @gate !enableRefAsProp || !__DEV__ it('warns when giving a ref (complex)', async () => { - // defaultProps means this won't use SimpleMemoComponent (as of this writing) - // SimpleMemoComponent is unobservable tho, so we can't check :) function App() { return null; } - App.defaultProps = {}; - App = React.memo(App); + // A custom compare function means this won't use SimpleMemoComponent (as of this writing) + // SimpleMemoComponent is unobservable tho, so we can't check :) + App = React.memo(App, () => false); function Outer() { return {}} />; } ReactNoop.render(); await expect(async () => await waitForAll([])).toErrorDev([ - 'App: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.', 'Warning: Function components cannot be given refs. Attempts to access ' + 'this ref will fail.', ]); @@ -409,6 +407,7 @@ describe('memo', () => { expect(ReactNoop).toMatchRenderedOutput(); }); + // @gate !disableDefaultPropsExceptForClasses it('supports defaultProps defined on the memo() return value', async () => { function Counter({a, b, c, d, e}) { return ; @@ -483,6 +482,7 @@ describe('memo', () => { ); }); + // @gate !disableDefaultPropsExceptForClasses it('handles nested defaultProps declarations', async () => { function Inner(props) { return props.inner + props.middle + props.outer; diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 529eaef8a692b..74bf28d7ead00 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -143,6 +143,7 @@ import { enablePostpone, enableRenderableContext, enableRefAsProp, + disableDefaultPropsExceptForClasses, } from 'shared/ReactFeatureFlags'; import assign from 'shared/assign'; @@ -1396,12 +1397,23 @@ export function resolveClassComponentProps( ): Object { let newProps = baseProps; - // TODO: This is where defaultProps should be resolved, too. + // Resolve default props. Taken from old JSX runtime, where this used to live. + const defaultProps = Component.defaultProps; + if (defaultProps && disableDefaultPropsExceptForClasses) { + newProps = assign({}, newProps, baseProps); + for (const propName in defaultProps) { + if (newProps[propName] === undefined) { + newProps[propName] = defaultProps[propName]; + } + } + } if (enableRefAsProp) { // Remove ref from the props object, if it exists. if ('ref' in newProps) { - newProps = assign({}, newProps); + if (newProps === baseProps) { + newProps = assign({}, newProps); + } delete newProps.ref; } } @@ -1629,7 +1641,15 @@ function validateFunctionComponentInDev(Component: any): void { } } -function resolveDefaultProps(Component: any, baseProps: Object): Object { +function resolveDefaultPropsOnNonClassComponent( + Component: any, + baseProps: Object, +): Object { + if (disableDefaultPropsExceptForClasses) { + // Support for defaultProps is removed in React 19 for all types + // except classes. + return baseProps; + } if (Component && Component.defaultProps) { // Resolve default props. Taken from ReactElement const props = assign({}, baseProps); @@ -1705,7 +1725,10 @@ function renderMemo( ref: any, ): void { const innerType = type.type; - const resolvedProps = resolveDefaultProps(innerType, props); + const resolvedProps = resolveDefaultPropsOnNonClassComponent( + innerType, + props, + ); renderElement(request, task, keyPath, innerType, resolvedProps, ref); } @@ -1779,7 +1802,10 @@ function renderLazyComponent( const payload = lazyComponent._payload; const init = lazyComponent._init; const Component = init(payload); - const resolvedProps = resolveDefaultProps(Component, props); + const resolvedProps = resolveDefaultPropsOnNonClassComponent( + Component, + props, + ); renderElement(request, task, keyPath, Component, resolvedProps, ref); task.componentStack = previousComponentStack; } diff --git a/packages/react/src/ReactLazy.js b/packages/react/src/ReactLazy.js index 5b5ef2b8caac7..e1b222ad8b222 100644 --- a/packages/react/src/ReactLazy.js +++ b/packages/react/src/ReactLazy.js @@ -10,6 +10,7 @@ import type {Wakeable, Thenable, ReactDebugInfo} from 'shared/ReactTypes'; import {REACT_LAZY_TYPE} from 'shared/ReactSymbols'; +import {disableDefaultPropsExceptForClasses} from 'shared/ReactFeatureFlags'; const Uninitialized = -1; const Pending = 0; @@ -134,53 +135,34 @@ export function lazy( _init: lazyInitializer, }; - if (__DEV__) { - // In production, this would just set it on the object. - let defaultProps; - let propTypes; - // $FlowFixMe[prop-missing] - Object.defineProperties(lazyType, { - defaultProps: { - configurable: true, - get() { - return defaultProps; - }, - // $FlowFixMe[missing-local-annot] - set(newDefaultProps) { - console.error( - 'It is not supported to assign `defaultProps` to ' + - 'a lazy component import. Either specify them where the component ' + - 'is defined, or create a wrapping component around it.', - ); - defaultProps = newDefaultProps; - // Match production behavior more closely: - // $FlowFixMe[prop-missing] - Object.defineProperty(lazyType, 'defaultProps', { - enumerable: true, - }); - }, - }, - propTypes: { - configurable: true, - get() { - return propTypes; - }, - // $FlowFixMe[missing-local-annot] - set(newPropTypes) { - console.error( - 'It is not supported to assign `propTypes` to ' + - 'a lazy component import. Either specify them where the component ' + - 'is defined, or create a wrapping component around it.', - ); - propTypes = newPropTypes; - // Match production behavior more closely: - // $FlowFixMe[prop-missing] - Object.defineProperty(lazyType, 'propTypes', { - enumerable: true, - }); + if (!disableDefaultPropsExceptForClasses) { + if (__DEV__) { + // In production, this would just set it on the object. + let defaultProps; + // $FlowFixMe[prop-missing] + Object.defineProperties(lazyType, { + defaultProps: { + configurable: true, + get() { + return defaultProps; + }, + // $FlowFixMe[missing-local-annot] + set(newDefaultProps) { + console.error( + 'It is not supported to assign `defaultProps` to ' + + 'a lazy component import. Either specify them where the component ' + + 'is defined, or create a wrapping component around it.', + ); + defaultProps = newDefaultProps; + // Match production behavior more closely: + // $FlowFixMe[prop-missing] + Object.defineProperty(lazyType, 'defaultProps', { + enumerable: true, + }); + }, }, - }, - }); + }); + } } return lazyType; diff --git a/packages/react/src/__tests__/ReactElementClone-test.js b/packages/react/src/__tests__/ReactElementClone-test.js index f16423ee60256..8dd9fceebc9c5 100644 --- a/packages/react/src/__tests__/ReactElementClone-test.js +++ b/packages/react/src/__tests__/ReactElementClone-test.js @@ -294,6 +294,7 @@ describe('ReactElementClone', () => { ); }); + // @gate !disableDefaultPropsExceptForClasses it('should normalize props with default values', () => { class Component extends React.Component { render() { diff --git a/packages/react/src/__tests__/forwardRef-test.js b/packages/react/src/__tests__/forwardRef-test.js index 667da7bb48236..15726519eeb98 100644 --- a/packages/react/src/__tests__/forwardRef-test.js +++ b/packages/react/src/__tests__/forwardRef-test.js @@ -74,6 +74,7 @@ describe('forwardRef', () => { expect(ref.current).toBe(null); }); + // @gate !disableDefaultPropsExceptForClasses it('should support defaultProps', async () => { function FunctionComponent({forwardedRef, optional, required}) { return ( diff --git a/packages/react/src/jsx/ReactJSXElement.js b/packages/react/src/jsx/ReactJSXElement.js index 3a0027c2b6a32..ffee6e1379706 100644 --- a/packages/react/src/jsx/ReactJSXElement.js +++ b/packages/react/src/jsx/ReactJSXElement.js @@ -18,7 +18,11 @@ import {checkKeyStringCoercion} from 'shared/CheckStringCoercion'; import isValidElementType from 'shared/isValidElementType'; import isArray from 'shared/isArray'; import {describeUnknownElementTypeFrameInDEV} from 'shared/ReactComponentStackFrame'; -import {enableRefAsProp, disableStringRefs} from 'shared/ReactFeatureFlags'; +import { + enableRefAsProp, + disableStringRefs, + disableDefaultPropsExceptForClasses, +} from 'shared/ReactFeatureFlags'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame; @@ -340,12 +344,14 @@ export function jsxProd(type, config, maybeKey) { } } - // Resolve default props - if (type && type.defaultProps) { - const defaultProps = type.defaultProps; - for (propName in defaultProps) { - if (props[propName] === undefined) { - props[propName] = defaultProps[propName]; + if (!disableDefaultPropsExceptForClasses) { + // Resolve default props + if (type && type.defaultProps) { + const defaultProps = type.defaultProps; + for (propName in defaultProps) { + if (props[propName] === undefined) { + props[propName] = defaultProps[propName]; + } } } } @@ -554,12 +560,14 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) { } } - // Resolve default props - if (type && type.defaultProps) { - const defaultProps = type.defaultProps; - for (propName in defaultProps) { - if (props[propName] === undefined) { - props[propName] = defaultProps[propName]; + if (!disableDefaultPropsExceptForClasses) { + // Resolve default props + if (type && type.defaultProps) { + const defaultProps = type.defaultProps; + for (propName in defaultProps) { + if (props[propName] === undefined) { + props[propName] = defaultProps[propName]; + } } } } @@ -811,7 +819,11 @@ export function cloneElement(element, config, children) { // Remaining properties override existing props let defaultProps; - if (element.type && element.type.defaultProps) { + if ( + !disableDefaultPropsExceptForClasses && + element.type && + element.type.defaultProps + ) { defaultProps = element.type.defaultProps; } for (propName in config) { @@ -833,7 +845,11 @@ export function cloneElement(element, config, children) { // backwards compatibility. !(enableRefAsProp && propName === 'ref' && config.ref === undefined) ) { - if (config[propName] === undefined && defaultProps !== undefined) { + if ( + !disableDefaultPropsExceptForClasses && + config[propName] === undefined && + defaultProps !== undefined + ) { // Resolve default props props[propName] = defaultProps[propName]; } else {