From 9974f1b643130bf6983bfd50b767d197087d0778 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 2 Apr 2024 14:07:02 -0400 Subject: [PATCH 1/2] Add disableDefaultPropsExceptForClasses flag --- packages/shared/ReactFeatureFlags.js | 3 +++ packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js | 1 + packages/shared/forks/ReactFeatureFlags.native-fb.js | 1 + packages/shared/forks/ReactFeatureFlags.native-oss.js | 1 + packages/shared/forks/ReactFeatureFlags.test-renderer.js | 1 + .../shared/forks/ReactFeatureFlags.test-renderer.native-fb.js | 2 ++ packages/shared/forks/ReactFeatureFlags.test-renderer.www.js | 2 ++ packages/shared/forks/ReactFeatureFlags.www-dynamic.js | 1 + packages/shared/forks/ReactFeatureFlags.www.js | 1 + 9 files changed, 13 insertions(+) diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index a55e864806457..fb52f57a9acc6 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 2dfb0f2bd2a0d..30b9d1a95458c 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js @@ -28,3 +28,4 @@ export const enableUnifiedSyncLane = __VARIANT__; export const enableUseRefAccessWarning = __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 62b554010caa2..5f641bc8fb685 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -30,6 +30,7 @@ export const { enableUseRefAccessWarning, 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 f89969af59b9c..6c4adbf9e874a 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 f93977ed1925e..ff93b1f92caf3 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -92,6 +92,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 2b85343516ab7..16183d8da3476 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -86,5 +86,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 9a757423fe7d7..36fabcd36bfdd 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -87,5 +87,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 96921a847d93c..54f665ff0fb6a 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -28,6 +28,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 39fcd219eae38..13d6ae038e710 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -35,6 +35,7 @@ export const { enableRenderableContext, enableRefAsProp, favorSafetyOverHydrationPerf, + disableDefaultPropsExceptForClasses, } = dynamicFeatureFlags; // On WWW, __EXPERIMENTAL__ is used for a new modern build. From 41b4a691b2ccf01a224a4e7c6e4f8e979a125876 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 3 Apr 2024 12:29:39 -0400 Subject: [PATCH 2/2] Remove defaultProps support (except for classes) This removes defaultProps support for all component types except for classes. We've chosen to continue supporting defaultProps for classes because lots of older code relies on it, and unlike function components, (which can use default params), there's no straightforward alternative. By implication, it also removes support for setting defaultProps on `React.lazy` wrapper. So this will not work: ```js const MyClassComponent = React.lazy(() => import('./MyClassComponent')); // MyClassComponent is not actually a class; it's a lazy wrapper. So // defaultProps does not work. MyClassComponent.defaultProps = { foo: 'bar' }; ``` However, if you set the default props on the class itself, then it's fine. For classes, this change also moves where defaultProps are resolved. Previously, defaultProps were resolved by the JSX runtime. This change is only observable if you introspect a JSX element, which is relatively rare but does happen. In other words, previously `.props.aDefaultProp` would resolve to the default prop value, but now it does not. --- .../ReactHooksInspectionIntegration-test.js | 1 + .../src/__tests__/ReactDOMFizzServer-test.js | 113 +++++++----------- .../ReactDeprecationWarnings-test.js | 2 + .../__tests__/ReactFunctionComponent-test.js | 2 + .../src/ReactFiberBeginWork.js | 66 ++++++---- .../src/ReactFiberClassComponent.js | 8 +- .../src/ReactFiberLazyComponent.js | 15 ++- .../src/ReactFiberWorkLoop.js | 9 +- .../src/__tests__/ReactLazy-test.internal.js | 52 ++++---- .../src/__tests__/ReactMemo-test.js | 10 +- packages/react-server/src/ReactFizzServer.js | 41 ++++++- packages/react/src/ReactLazy.js | 74 +++++------- .../src/__tests__/ReactElementClone-test.js | 1 + .../react/src/__tests__/forwardRef-test.js | 1 + packages/react/src/jsx/ReactJSXElement.js | 46 ++++--- 15 files changed, 243 insertions(+), 198 deletions(-) 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..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 8c7ba485856f2..140176ea10c21 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, ); } @@ -1913,7 +1928,10 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) { } } - if (Component.defaultProps !== undefined) { + if ( + !disableDefaultPropsExceptForClasses && + Component.defaultProps !== undefined + ) { const componentName = getComponentNameFromType(Component) || 'Unknown'; if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) { @@ -3928,9 +3946,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 +3998,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 +4024,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..788dfd6cf4051 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 {