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 dab01e4bd9779..f44605571f9f0 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..39a068e1c7ed8 100644 --- a/packages/react-dom/src/__tests__/ReactDeprecationWarnings-test.js +++ b/packages/react-dom/src/__tests__/ReactDeprecationWarnings-test.js @@ -26,6 +26,7 @@ describe('ReactDeprecationWarnings', () => { } }); + // @gate !disableDefaultPropsExceptForClasses || !__DEV__ it('should warn when given defaultProps', async () => { function FunctionalComponent(props) { return null; @@ -43,6 +44,7 @@ describe('ReactDeprecationWarnings', () => { ); }); + // @gate !disableDefaultPropsExceptForClasses || !__DEV__ 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-dom/src/__tests__/ReactFunctionComponent-test.js b/packages/react-dom/src/__tests__/ReactFunctionComponent-test.js index b1409c580c709..d353f6f8c6ee6 100644 --- a/packages/react-dom/src/__tests__/ReactFunctionComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactFunctionComponent-test.js @@ -433,6 +433,7 @@ describe('ReactFunctionComponent', () => { ); }); + // @gate !disableDefaultPropsExceptForClasses it('should support default props', async () => { function Child(props) { return
{props.test}
; @@ -446,6 +447,7 @@ describe('ReactFunctionComponent', () => { await act(() => { root.render(); }); + expect(container.textContent).toBe('2'); }).toErrorDev([ 'Warning: Child: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.', ]); diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 93740b161c732..6684d5595cc6f 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, @@ -487,7 +488,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__) { @@ -509,16 +511,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; + } } } } @@ -1767,7 +1771,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); @@ -1785,7 +1791,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 = @@ -1799,13 +1807,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, ); } @@ -1901,7 +1916,10 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) { } } - if (Component.defaultProps !== undefined) { + if ( + !disableDefaultPropsExceptForClasses && + Component.defaultProps !== undefined + ) { const componentName = getComponentNameFromType(Component) || 'Unknown'; if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) { @@ -3898,9 +3916,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, @@ -3949,9 +3968,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, @@ -3974,8 +3994,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 41d9bcdeaf8bf..e4b534fa9c33f 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, @@ -2411,9 +2412,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); @@ -2437,9 +2439,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 00f00522f313f..327311d74fa31 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; } } @@ -1587,7 +1599,10 @@ function validateFunctionComponentInDev(Component: any): void { } } - if (Component.defaultProps !== undefined) { + if ( + !disableDefaultPropsExceptForClasses && + Component.defaultProps !== undefined + ) { const componentName = getComponentNameFromType(Component) || 'Unknown'; if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) { @@ -1629,7 +1644,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 +1728,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 +1805,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 { diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index c527d069dcaa5..43cbe66ea47a1 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -40,6 +40,9 @@ export const disableSchedulerTimeoutInWorkLoop = false; // those can be fixed. export const enableDeferRootSchedulingToMicrotask = true; +// TODO: Land at Meta before removing. +export const disableDefaultPropsExceptForClasses = true; + // ----------------------------------------------------------------------------- // Slated for removal in the future (significant effort) // diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js b/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js index 50c65b0038e16..f024eb4b1cd87 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js @@ -27,3 +27,4 @@ export const enableRenderableContext = __VARIANT__; export const enableUnifiedSyncLane = __VARIANT__; export const passChildrenWhenCloningPersistedNodes = __VARIANT__; export const useModernStrictMode = __VARIANT__; +export const disableDefaultPropsExceptForClasses = __VARIANT__; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 004c76d747569..c3aee2ab41bee 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -29,6 +29,7 @@ export const { enableUnifiedSyncLane, passChildrenWhenCloningPersistedNodes, useModernStrictMode, + disableDefaultPropsExceptForClasses, } = dynamicFlags; // The rest of the flags are static for better dead code elimination. diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 79f77bdc052ca..6f5293398b8c8 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -37,6 +37,7 @@ export const enableComponentStackLocations = __TODO_NEXT_RN_MAJOR__; // ----------------------------------------------------------------------------- export const enableCache = __TODO_NEXT_RN_MAJOR__; export const enableRenderableContext = __TODO_NEXT_RN_MAJOR__; +export const disableDefaultPropsExceptForClasses = __TODO_NEXT_RN_MAJOR__; // ----------------------------------------------------------------------------- // Already enabled for next React Native major. diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index cef2b788b73ce..1fe4a6e2d86d9 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -90,6 +90,7 @@ export const disableLegacyContext = true; export const disableDOMTestUtils = true; export const enableRenderableContext = true; export const enableReactTestRendererWarning = true; +export const disableDefaultPropsExceptForClasses = true; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index d12550dabbc1d..e78740263ff90 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -85,5 +85,7 @@ export const enableReactTestRendererWarning = false; export const disableLegacyMode = false; export const disableDOMTestUtils = false; +export const disableDefaultPropsExceptForClasses = false; + // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 3bfc66a0069ba..a222f31812407 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -85,5 +85,7 @@ export const enableReactTestRendererWarning = false; export const disableLegacyMode = false; export const disableDOMTestUtils = false; +export const disableDefaultPropsExceptForClasses = false; + // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index b7e953047e77d..63f2d57ea4eb6 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -27,6 +27,7 @@ export const enableRenderableContext = __VARIANT__; export const enableRefAsProp = __VARIANT__; export const enableRetryLaneExpiration = __VARIANT__; export const favorSafetyOverHydrationPerf = __VARIANT__; +export const disableDefaultPropsExceptForClasses = __VARIANT__; export const retryLaneExpirationMs = 5000; export const syncLaneExpirationMs = 250; export const transitionLaneExpirationMs = 5000; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 3057542f39740..9d539dd8ecb78 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -34,6 +34,7 @@ export const { enableRenderableContext, enableRefAsProp, favorSafetyOverHydrationPerf, + disableDefaultPropsExceptForClasses, } = dynamicFeatureFlags; // On WWW, __EXPERIMENTAL__ is used for a new modern build.