@samitouri / QOS-React-2 / commits / 48b4ecc901

Remove defaultProps support (except for classes) (#28733)

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 `<ClassWithDefaultProp />.props.aDefaultProp` would resolve to the default prop value, but now it does not.

Andrew Clark committed Apr 4, 2024 at 10:59 UTC 48b4ecc9012638ed51b275aad24b2086b8215e32
24 files changed +256 -198
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+1
@@ -2293,6 +2293,7 @@ describe('ReactHooksInspectionIntegration', () => {
2293 });
2294 });
2295
2296 + // @gate !disableDefaultPropsExceptForClasses
2297 it('should support defaultProps and lazy', async () => {
2298 const Suspense = React.Suspense;
2299
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+43 -70
@@ -405,18 +405,6 @@ describe('ReactDOMFizzServer', () => {
405 }
406
407 it('should asynchronously load a lazy component', async () => {
408 - const originalConsoleError = console.error;
409 - const mockError = jest.fn();
410 - console.error = (...args) => {
411 - if (args.length > 1) {
412 - if (typeof args[1] === 'object') {
413 - mockError(args[0].split('\n')[0]);
414 - return;
415 - }
416 - }
417 - mockError(...args.map(normalizeCodeLocInfo));
418 - };
419 -
408 let resolveA;
409 const LazyA = React.lazy(() => {
410 return new Promise(r => {
@@ -431,74 +419,59 @@ describe('ReactDOMFizzServer', () => {
419 });
420 });
421
434 - function TextWithPunctuation({text, punctuation}) {
435 - return <Text text={text + punctuation} />;
422 + class TextWithPunctuation extends React.Component {
423 + render() {
424 + return <Text text={this.props.text + this.props.punctuation} />;
425 + }
426 }
427 +
428 // This tests that default props of the inner element is resolved.
429 TextWithPunctuation.defaultProps = {
430 punctuation: '!',
431 };
432
442 - try {
443 - await act(() => {
444 - const {pipe} = renderToPipeableStream(
445 - <div>
446 - <div>
447 - <Suspense fallback={<Text text="Loading..." />}>
448 - <LazyA text="Hello" />
449 - </Suspense>
450 - </div>
451 - <div>
452 - <Suspense fallback={<Text text="Loading..." />}>
453 - <LazyB text="world" />
454 - </Suspense>
455 - </div>
456 - </div>,
457 - );
458 - pipe(writable);
459 - });
460 -
461 - expect(getVisibleChildren(container)).toEqual(
462 - <div>
463 - <div>Loading...</div>
464 - <div>Loading...</div>
465 - </div>,
466 - );
467 - await act(() => {
468 - resolveA({default: Text});
469 - });
470 - expect(getVisibleChildren(container)).toEqual(
471 - <div>
472 - <div>Hello</div>
473 - <div>Loading...</div>
474 - </div>,
475 - );
476 - await act(() => {
477 - resolveB({default: TextWithPunctuation});
478 - });
479 - expect(getVisibleChildren(container)).toEqual(
433 + await act(() => {
434 + const {pipe} = renderToPipeableStream(
435 <div>
481 - <div>Hello</div>
482 - <div>world!</div>
436 + <div>
437 + <Suspense fallback={<Text text="Loading..." />}>
438 + <LazyA text="Hello" />
439 + </Suspense>
440 + </div>
441 + <div>
442 + <Suspense fallback={<Text text="Loading..." />}>
443 + <LazyB text="world" />
444 + </Suspense>
445 + </div>
446 </div>,
447 );
448 + pipe(writable);
449 + });
450
486 - if (__DEV__) {
487 - expect(mockError).toHaveBeenCalledWith(
488 - 'Warning: %s: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.%s',
489 - 'TextWithPunctuation',
490 - '\n in TextWithPunctuation (at **)\n' +
491 - ' in Lazy (at **)\n' +
492 - ' in Suspense (at **)\n' +
493 - ' in div (at **)\n' +
494 - ' in div (at **)',
495 - );
496 - } else {
497 - expect(mockError).not.toHaveBeenCalled();
498 - }
499 - } finally {
500 - console.error = originalConsoleError;
501 - }
451 + expect(getVisibleChildren(container)).toEqual(
452 + <div>
453 + <div>Loading...</div>
454 + <div>Loading...</div>
455 + </div>,
456 + );
457 + await act(() => {
458 + resolveA({default: Text});
459 + });
460 + expect(getVisibleChildren(container)).toEqual(
461 + <div>
462 + <div>Hello</div>
463 + <div>Loading...</div>
464 + </div>,
465 + );
466 + await act(() => {
467 + resolveB({default: TextWithPunctuation});
468 + });
469 + expect(getVisibleChildren(container)).toEqual(
470 + <div>
471 + <div>Hello</div>
472 + <div>world!</div>
473 + </div>,
474 + );
475 });
476
477 it('#23331: does not warn about hydration mismatches if something suspended in an earlier sibling', async () => {
packages/react-dom/src/__tests__/ReactDeprecationWarnings-test.js
+2
@@ -26,6 +26,7 @@ describe('ReactDeprecationWarnings', () => {
26 }
27 });
28
29 + // @gate !disableDefaultPropsExceptForClasses || !__DEV__
30 it('should warn when given defaultProps', async () => {
31 function FunctionalComponent(props) {
32 return null;
@@ -43,6 +44,7 @@ describe('ReactDeprecationWarnings', () => {
44 );
45 });
46
47 + // @gate !disableDefaultPropsExceptForClasses || !__DEV__
48 it('should warn when given defaultProps on a memoized function', async () => {
49 const MemoComponent = React.memo(function FunctionalComponent(props) {
50 return null;
packages/react-dom/src/__tests__/ReactFunctionComponent-test.js
+2
@@ -433,6 +433,7 @@ describe('ReactFunctionComponent', () => {
433 );
434 });
435
436 + // @gate !disableDefaultPropsExceptForClasses
437 it('should support default props', async () => {
438 function Child(props) {
439 return <div>{props.test}</div>;
@@ -446,6 +447,7 @@ describe('ReactFunctionComponent', () => {
447 await act(() => {
448 root.render(<Child />);
449 });
450 + expect(container.textContent).toBe('2');
451 }).toErrorDev([
452 'Warning: Child: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.',
453 ]);
packages/react-reconciler/src/ReactFiberBeginWork.js
+45 -21
@@ -109,6 +109,7 @@ import {
109 enableRenderableContext,
110 enableRefAsProp,
111 disableLegacyMode,
112 + disableDefaultPropsExceptForClasses,
113 } from 'shared/ReactFeatureFlags';
114 import isArray from 'shared/isArray';
115 import shallowEqual from 'shared/shallowEqual';
@@ -247,7 +248,7 @@ import {
248 updateClassInstance,
249 resolveClassComponentProps,
250 } from './ReactFiberClassComponent';
250 -import {resolveDefaultProps} from './ReactFiberLazyComponent';
251 +import {resolveDefaultPropsOnNonClassComponent} from './ReactFiberLazyComponent';
252 import {
253 createFiberFromTypeAndProps,
254 createFiberFromFragment,
@@ -487,7 +488,8 @@ function updateMemoComponent(
488 isSimpleFunctionComponent(type) &&
489 Component.compare === null &&
490 // SimpleMemoComponent codepath doesn't resolve outer props either.
490 - Component.defaultProps === undefined
491 + (disableDefaultPropsExceptForClasses ||
492 + Component.defaultProps === undefined)
493 ) {
494 let resolvedType = type;
495 if (__DEV__) {
@@ -509,16 +511,18 @@ function updateMemoComponent(
511 renderLanes,
512 );
513 }
512 - if (__DEV__) {
513 - if (Component.defaultProps !== undefined) {
514 - const componentName = getComponentNameFromType(type) || 'Unknown';
515 - if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) {
516 - console.error(
517 - '%s: Support for defaultProps will be removed from memo components ' +
518 - 'in a future major release. Use JavaScript default parameters instead.',
519 - componentName,
520 - );
521 - didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true;
514 + if (!disableDefaultPropsExceptForClasses) {
515 + if (__DEV__) {
516 + if (Component.defaultProps !== undefined) {
517 + const componentName = getComponentNameFromType(type) || 'Unknown';
518 + if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) {
519 + console.error(
520 + '%s: Support for defaultProps will be removed from memo components ' +
521 + 'in a future major release. Use JavaScript default parameters instead.',
522 + componentName,
523 + );
524 + didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true;
525 + }
526 }
527 }
528 }
@@ -1766,7 +1770,9 @@ function mountLazyComponent(
1770 renderLanes,
1771 );
1772 } else {
1769 - const resolvedProps = resolveDefaultProps(Component, props);
1773 + const resolvedProps = disableDefaultPropsExceptForClasses
1774 + ? props
1775 + : resolveDefaultPropsOnNonClassComponent(Component, props);
1776 workInProgress.tag = FunctionComponent;
1777 if (__DEV__) {
1778 validateFunctionComponentInDev(workInProgress, Component);
@@ -1784,7 +1790,9 @@ function mountLazyComponent(
1790 } else if (Component !== undefined && Component !== null) {
1791 const $$typeof = Component.$$typeof;
1792 if ($$typeof === REACT_FORWARD_REF_TYPE) {
1787 - const resolvedProps = resolveDefaultProps(Component, props);
1793 + const resolvedProps = disableDefaultPropsExceptForClasses
1794 + ? props
1795 + : resolveDefaultPropsOnNonClassComponent(Component, props);
1796 workInProgress.tag = ForwardRef;
1797 if (__DEV__) {
1798 workInProgress.type = Component =
@@ -1798,13 +1806,20 @@ function mountLazyComponent(
1806 renderLanes,
1807 );
1808 } else if ($$typeof === REACT_MEMO_TYPE) {
1801 - const resolvedProps = resolveDefaultProps(Component, props);
1809 + const resolvedProps = disableDefaultPropsExceptForClasses
1810 + ? props
1811 + : resolveDefaultPropsOnNonClassComponent(Component, props);
1812 workInProgress.tag = MemoComponent;
1813 return updateMemoComponent(
1814 null,
1815 workInProgress,
1816 Component,
1807 - resolveDefaultProps(Component.type, resolvedProps), // The inner type can have defaults too
1817 + disableDefaultPropsExceptForClasses
1818 + ? resolvedProps
1819 + : resolveDefaultPropsOnNonClassComponent(
1820 + Component.type,
1821 + resolvedProps,
1822 + ), // The inner type can have defaults too
1823 renderLanes,
1824 );
1825 }
@@ -1900,7 +1915,10 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {
1915 }
1916 }
1917
1903 - if (Component.defaultProps !== undefined) {
1918 + if (
1919 + !disableDefaultPropsExceptForClasses &&
1920 + Component.defaultProps !== undefined
1921 + ) {
1922 const componentName = getComponentNameFromType(Component) || 'Unknown';
1923
1924 if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) {
@@ -3897,9 +3915,10 @@ function beginWork(
3915 const Component = workInProgress.type;
3916 const unresolvedProps = workInProgress.pendingProps;
3917 const resolvedProps =
3918 + disableDefaultPropsExceptForClasses ||
3919 workInProgress.elementType === Component
3920 ? unresolvedProps
3902 - : resolveDefaultProps(Component, unresolvedProps);
3921 + : resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps);
3922 return updateFunctionComponent(
3923 current,
3924 workInProgress,
@@ -3948,9 +3967,10 @@ function beginWork(
3967 const type = workInProgress.type;
3968 const unresolvedProps = workInProgress.pendingProps;
3969 const resolvedProps =
3970 + disableDefaultPropsExceptForClasses ||
3971 workInProgress.elementType === type
3972 ? unresolvedProps
3953 - : resolveDefaultProps(type, unresolvedProps);
3973 + : resolveDefaultPropsOnNonClassComponent(type, unresolvedProps);
3974 return updateForwardRef(
3975 current,
3976 workInProgress,
@@ -3973,8 +3993,12 @@ function beginWork(
3993 const type = workInProgress.type;
3994 const unresolvedProps = workInProgress.pendingProps;
3995 // Resolve outer props first, then resolve inner props.
3976 - let resolvedProps = resolveDefaultProps(type, unresolvedProps);
3977 - resolvedProps = resolveDefaultProps(type.type, resolvedProps);
3996 + let resolvedProps = disableDefaultPropsExceptForClasses
3997 + ? unresolvedProps
3998 + : resolveDefaultPropsOnNonClassComponent(type, unresolvedProps);
3999 + resolvedProps = disableDefaultPropsExceptForClasses
4000 + ? resolvedProps
4001 + : resolveDefaultPropsOnNonClassComponent(type.type, resolvedProps);
4002 return updateMemoComponent(
4003 current,
4004 workInProgress,
packages/react-reconciler/src/ReactFiberClassComponent.js
+7 -1
@@ -24,6 +24,7 @@ import {
24 enableSchedulingProfiler,
25 enableLazyContextPropagation,
26 enableRefAsProp,
27 + disableDefaultPropsExceptForClasses,
28 } from 'shared/ReactFeatureFlags';
29 import ReactStrictModeWarnings from './ReactStrictModeWarnings';
30 import {isMounted} from './ReactFiberTreeReflection';
@@ -1252,7 +1253,12 @@ export function resolveClassComponentProps(
1253
1254 // Resolve default props. Taken from old JSX runtime, where this used to live.
1255 const defaultProps = Component.defaultProps;
1255 - if (defaultProps && !alreadyResolvedDefaultProps) {
1256 + if (
1257 + defaultProps &&
1258 + // If disableDefaultPropsExceptForClasses is true, we always resolve
1259 + // default props here in the reconciler, rather than in the JSX runtime.
1260 + (disableDefaultPropsExceptForClasses || !alreadyResolvedDefaultProps)
1261 + ) {
1262 newProps = assign({}, newProps, baseProps);
1263 for (const propName in defaultProps) {
1264 if (newProps[propName] === undefined) {
packages/react-reconciler/src/ReactFiberLazyComponent.js
+10 -5
@@ -8,12 +8,17 @@
8 */
9
10 import assign from 'shared/assign';
11 +import {disableDefaultPropsExceptForClasses} from 'shared/ReactFeatureFlags';
12
12 -export function resolveDefaultProps(Component: any, baseProps: Object): Object {
13 - // TODO: Remove support for default props for everything except class
14 - // components, including setting default props on a lazy wrapper around a
15 - // class type.
16 -
13 +export function resolveDefaultPropsOnNonClassComponent(
14 + Component: any,
15 + baseProps: Object,
16 +): Object {
17 + if (disableDefaultPropsExceptForClasses) {
18 + // Support for defaultProps is removed in React 19 for all types
19 + // except classes.
20 + return baseProps;
21 + }
22 if (Component && Component.defaultProps) {
23 // Resolve default props. Taken from ReactElement
24 const props = assign({}, baseProps);
packages/react-reconciler/src/ReactFiberWorkLoop.js
+6 -3
@@ -40,6 +40,7 @@ import {
40 alwaysThrottleRetries,
41 enableInfiniteRenderLoopDetection,
42 disableLegacyMode,
43 + disableDefaultPropsExceptForClasses,
44 } from 'shared/ReactFeatureFlags';
45 import ReactSharedInternals from 'shared/ReactSharedInternals';
46 import is from 'shared/objectIs';
@@ -264,7 +265,7 @@ import {
265 getSuspenseHandler,
266 getShellBoundary,
267 } from './ReactFiberSuspenseContext';
267 -import {resolveDefaultProps} from './ReactFiberLazyComponent';
268 +import {resolveDefaultPropsOnNonClassComponent} from './ReactFiberLazyComponent';
269 import {resetChildReconcilerOnUnwind} from './ReactChildFiber';
270 import {
271 ensureRootIsScheduled,
@@ -2411,9 +2412,10 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
2412 const Component = unitOfWork.type;
2413 const unresolvedProps = unitOfWork.pendingProps;
2414 const resolvedProps =
2415 + disableDefaultPropsExceptForClasses ||
2416 unitOfWork.elementType === Component
2417 ? unresolvedProps
2416 - : resolveDefaultProps(Component, unresolvedProps);
2418 + : resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps);
2419 let context: any;
2420 if (!disableLegacyContext) {
2421 const unmaskedContext = getUnmaskedContext(unitOfWork, Component, true);
@@ -2437,9 +2439,10 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
2439 const Component = unitOfWork.type.render;
2440 const unresolvedProps = unitOfWork.pendingProps;
2441 const resolvedProps =
2442 + disableDefaultPropsExceptForClasses ||
2443 unitOfWork.elementType === Component
2444 ? unresolvedProps
2442 - : resolveDefaultProps(Component, unresolvedProps);
2445 + : resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps);
2446
2447 next = replayFunctionComponent(
2448 current,
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+26 -26
@@ -328,8 +328,10 @@ describe('ReactLazy', () => {
328 });
329
330 it('resolves defaultProps, on mount and update', async () => {
331 - function T(props) {
332 - return <Text {...props} />;
331 + class T extends React.Component {
332 + render() {
333 + return <Text {...this.props} />;
334 + }
335 }
336 T.defaultProps = {text: 'Hi'};
337 const LazyText = lazy(() => fakeImport(T));
@@ -346,14 +348,8 @@ describe('ReactLazy', () => {
348 await waitForAll(['Loading...']);
349 expect(root).not.toMatchRenderedOutput('Hi');
350
349 - await expect(async () => {
350 - await act(() => resolveFakeImport(T));
351 - assertLog(['Hi']);
352 - }).toErrorDev(
353 - 'Warning: T: Support for defaultProps ' +
354 - 'will be removed from function components in a future major ' +
355 - 'release. Use JavaScript default parameters instead.',
356 - );
351 + await act(() => resolveFakeImport(T));
352 + assertLog(['Hi']);
353
354 expect(root).toMatchRenderedOutput('Hi');
355
@@ -368,14 +364,16 @@ describe('ReactLazy', () => {
364 });
365
366 it('resolves defaultProps without breaking memoization', async () => {
371 - function LazyImpl(props) {
372 - Scheduler.log('Lazy');
373 - return (
374 - <>
375 - <Text text={props.siblingText} />
376 - {props.children}
377 - </>
378 - );
367 + class LazyImpl extends React.Component {
368 + render() {
369 + Scheduler.log('Lazy');
370 + return (
371 + <>
372 + <Text text={this.props.siblingText} />
373 + {this.props.children}
374 + </>
375 + );
376 + }
377 }
378 LazyImpl.defaultProps = {siblingText: 'Sibling'};
379 const Lazy = lazy(() => fakeImport(LazyImpl));
@@ -402,14 +400,8 @@ describe('ReactLazy', () => {
400 await waitForAll(['Loading...']);
401 expect(root).not.toMatchRenderedOutput('SiblingA');
402
405 - await expect(async () => {
406 - await act(() => resolveFakeImport(LazyImpl));
407 - assertLog(['Lazy', 'Sibling', 'A']);
408 - }).toErrorDev(
409 - 'Warning: LazyImpl: Support for defaultProps ' +
410 - 'will be removed from function components in a future major ' +
411 - 'release. Use JavaScript default parameters instead.',
412 - );
403 + await act(() => resolveFakeImport(LazyImpl));
404 + assertLog(['Lazy', 'Sibling', 'A']);
405
406 expect(root).toMatchRenderedOutput('SiblingA');
407
@@ -680,6 +672,7 @@ describe('ReactLazy', () => {
672 expect(root).toMatchRenderedOutput('A3');
673 });
674
675 + // @gate !disableDefaultPropsExceptForClasses
676 it('resolves defaultProps on the outer wrapper but warns', async () => {
677 function T(props) {
678 Scheduler.log(props.inner + ' ' + props.outer);
@@ -837,6 +830,7 @@ describe('ReactLazy', () => {
830 expect(root).toMatchRenderedOutput('0');
831 }
832
833 + // @gate !disableDefaultPropsExceptForClasses
834 it('resolves props for function component with defaultProps', async () => {
835 function Add(props) {
836 expect(props.innerWithDefault).toBe(42);
@@ -877,6 +871,7 @@ describe('ReactLazy', () => {
871 await verifyResolvesProps(Add);
872 });
873
874 + // @gate !disableDefaultPropsExceptForClasses
875 it('resolves props for forwardRef component with defaultProps', async () => {
876 const Add = React.forwardRef((props, ref) => {
877 expect(props.innerWithDefault).toBe(42);
@@ -897,6 +892,7 @@ describe('ReactLazy', () => {
892 await verifyResolvesProps(Add);
893 });
894
895 + // @gate !disableDefaultPropsExceptForClasses
896 it('resolves props for outer memo component with defaultProps', async () => {
897 let Add = props => {
898 expect(props.innerWithDefault).toBe(42);
@@ -917,6 +913,7 @@ describe('ReactLazy', () => {
913 await verifyResolvesProps(Add);
914 });
915
916 + // @gate !disableDefaultPropsExceptForClasses
917 it('resolves props for inner memo component with defaultProps', async () => {
918 const Add = props => {
919 expect(props.innerWithDefault).toBe(42);
@@ -937,6 +934,7 @@ describe('ReactLazy', () => {
934 await verifyResolvesProps(React.memo(Add));
935 });
936
937 + // @gate !disableDefaultPropsExceptForClasses
938 it('uses outer resolved props on memo', async () => {
939 let T = props => {
940 return <Text text={props.text} />;
@@ -1052,6 +1050,7 @@ describe('ReactLazy', () => {
1050 });
1051
1052 // Regression test for #14310
1053 + // @gate !disableDefaultPropsExceptForClasses
1054 it('supports defaultProps defined on the memo() return value', async () => {
1055 const Add = React.memo(props => {
1056 return props.inner + props.outer;
@@ -1134,6 +1133,7 @@ describe('ReactLazy', () => {
1133 expect(root).toMatchRenderedOutput('3');
1134 });
1135
1136 + // @gate !disableDefaultPropsExceptForClasses
1137 it('merges defaultProps in the correct order', async () => {
1138 let Add = React.memo(props => {
1139 return props.inner + props.outer;
packages/react-reconciler/src/__tests__/ReactMemo-test.js
+5 -5
@@ -65,19 +65,17 @@ describe('memo', () => {
65
66 // @gate !enableRefAsProp || !__DEV__
67 it('warns when giving a ref (complex)', async () => {
68 - // defaultProps means this won't use SimpleMemoComponent (as of this writing)
69 - // SimpleMemoComponent is unobservable tho, so we can't check :)
68 function App() {
69 return null;
70 }
73 - App.defaultProps = {};
74 - App = React.memo(App);
71 + // A custom compare function means this won't use SimpleMemoComponent (as of this writing)
72 + // SimpleMemoComponent is unobservable tho, so we can't check :)
73 + App = React.memo(App, () => false);
74 function Outer() {
75 return <App ref={() => {}} />;
76 }
77 ReactNoop.render(<Outer />);
78 await expect(async () => await waitForAll([])).toErrorDev([
80 - 'App: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.',
79 'Warning: Function components cannot be given refs. Attempts to access ' +
80 'this ref will fail.',
81 ]);
@@ -409,6 +407,7 @@ describe('memo', () => {
407 expect(ReactNoop).toMatchRenderedOutput(<span prop="1!" />);
408 });
409
410 + // @gate !disableDefaultPropsExceptForClasses
411 it('supports defaultProps defined on the memo() return value', async () => {
412 function Counter({a, b, c, d, e}) {
413 return <Text text={a + b + c + d + e} />;
@@ -483,6 +482,7 @@ describe('memo', () => {
482 );
483 });
484
485 + // @gate !disableDefaultPropsExceptForClasses
486 it('handles nested defaultProps declarations', async () => {
487 function Inner(props) {
488 return props.inner + props.middle + props.outer;
packages/react-server/src/ReactFizzServer.js
+35 -6
@@ -143,6 +143,7 @@ import {
143 enablePostpone,
144 enableRenderableContext,
145 enableRefAsProp,
146 + disableDefaultPropsExceptForClasses,
147 } from 'shared/ReactFeatureFlags';
148
149 import assign from 'shared/assign';
@@ -1396,12 +1397,23 @@ export function resolveClassComponentProps(
1397 ): Object {
1398 let newProps = baseProps;
1399
1399 - // TODO: This is where defaultProps should be resolved, too.
1400 + // Resolve default props. Taken from old JSX runtime, where this used to live.
1401 + const defaultProps = Component.defaultProps;
1402 + if (defaultProps && disableDefaultPropsExceptForClasses) {
1403 + newProps = assign({}, newProps, baseProps);
1404 + for (const propName in defaultProps) {
1405 + if (newProps[propName] === undefined) {
1406 + newProps[propName] = defaultProps[propName];
1407 + }
1408 + }
1409 + }
1410
1411 if (enableRefAsProp) {
1412 // Remove ref from the props object, if it exists.
1413 if ('ref' in newProps) {
1404 - newProps = assign({}, newProps);
1414 + if (newProps === baseProps) {
1415 + newProps = assign({}, newProps);
1416 + }
1417 delete newProps.ref;
1418 }
1419 }
@@ -1587,7 +1599,10 @@ function validateFunctionComponentInDev(Component: any): void {
1599 }
1600 }
1601
1590 - if (Component.defaultProps !== undefined) {
1602 + if (
1603 + !disableDefaultPropsExceptForClasses &&
1604 + Component.defaultProps !== undefined
1605 + ) {
1606 const componentName = getComponentNameFromType(Component) || 'Unknown';
1607
1608 if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) {
@@ -1629,7 +1644,15 @@ function validateFunctionComponentInDev(Component: any): void {
1644 }
1645 }
1646
1632 -function resolveDefaultProps(Component: any, baseProps: Object): Object {
1647 +function resolveDefaultPropsOnNonClassComponent(
1648 + Component: any,
1649 + baseProps: Object,
1650 +): Object {
1651 + if (disableDefaultPropsExceptForClasses) {
1652 + // Support for defaultProps is removed in React 19 for all types
1653 + // except classes.
1654 + return baseProps;
1655 + }
1656 if (Component && Component.defaultProps) {
1657 // Resolve default props. Taken from ReactElement
1658 const props = assign({}, baseProps);
@@ -1705,7 +1728,10 @@ function renderMemo(
1728 ref: any,
1729 ): void {
1730 const innerType = type.type;
1708 - const resolvedProps = resolveDefaultProps(innerType, props);
1731 + const resolvedProps = resolveDefaultPropsOnNonClassComponent(
1732 + innerType,
1733 + props,
1734 + );
1735 renderElement(request, task, keyPath, innerType, resolvedProps, ref);
1736 }
1737
@@ -1779,7 +1805,10 @@ function renderLazyComponent(
1805 const payload = lazyComponent._payload;
1806 const init = lazyComponent._init;
1807 const Component = init(payload);
1782 - const resolvedProps = resolveDefaultProps(Component, props);
1808 + const resolvedProps = resolveDefaultPropsOnNonClassComponent(
1809 + Component,
1810 + props,
1811 + );
1812 renderElement(request, task, keyPath, Component, resolvedProps, ref);
1813 task.componentStack = previousComponentStack;
1814 }
packages/react/src/ReactLazy.js
+28 -46
@@ -10,6 +10,7 @@
10 import type {Wakeable, Thenable, ReactDebugInfo} from 'shared/ReactTypes';
11
12 import {REACT_LAZY_TYPE} from 'shared/ReactSymbols';
13 +import {disableDefaultPropsExceptForClasses} from 'shared/ReactFeatureFlags';
14
15 const Uninitialized = -1;
16 const Pending = 0;
@@ -134,53 +135,34 @@ export function lazy<T>(
135 _init: lazyInitializer,
136 };
137
137 - if (__DEV__) {
138 - // In production, this would just set it on the object.
139 - let defaultProps;
140 - let propTypes;
141 - // $FlowFixMe[prop-missing]
142 - Object.defineProperties(lazyType, {
143 - defaultProps: {
144 - configurable: true,
145 - get() {
146 - return defaultProps;
147 - },
148 - // $FlowFixMe[missing-local-annot]
149 - set(newDefaultProps) {
150 - console.error(
151 - 'It is not supported to assign `defaultProps` to ' +
152 - 'a lazy component import. Either specify them where the component ' +
153 - 'is defined, or create a wrapping component around it.',
154 - );
155 - defaultProps = newDefaultProps;
156 - // Match production behavior more closely:
157 - // $FlowFixMe[prop-missing]
158 - Object.defineProperty(lazyType, 'defaultProps', {
159 - enumerable: true,
160 - });
161 - },
162 - },
163 - propTypes: {
164 - configurable: true,
165 - get() {
166 - return propTypes;
167 - },
168 - // $FlowFixMe[missing-local-annot]
169 - set(newPropTypes) {
170 - console.error(
171 - 'It is not supported to assign `propTypes` to ' +
172 - 'a lazy component import. Either specify them where the component ' +
173 - 'is defined, or create a wrapping component around it.',
174 - );
175 - propTypes = newPropTypes;
176 - // Match production behavior more closely:
177 - // $FlowFixMe[prop-missing]
178 - Object.defineProperty(lazyType, 'propTypes', {
179 - enumerable: true,
180 - });
138 + if (!disableDefaultPropsExceptForClasses) {
139 + if (__DEV__) {
140 + // In production, this would just set it on the object.
141 + let defaultProps;
142 + // $FlowFixMe[prop-missing]
143 + Object.defineProperties(lazyType, {
144 + defaultProps: {
145 + configurable: true,
146 + get() {
147 + return defaultProps;
148 + },
149 + // $FlowFixMe[missing-local-annot]
150 + set(newDefaultProps) {
151 + console.error(
152 + 'It is not supported to assign `defaultProps` to ' +
153 + 'a lazy component import. Either specify them where the component ' +
154 + 'is defined, or create a wrapping component around it.',
155 + );
156 + defaultProps = newDefaultProps;
157 + // Match production behavior more closely:
158 + // $FlowFixMe[prop-missing]
159 + Object.defineProperty(lazyType, 'defaultProps', {
160 + enumerable: true,
161 + });
162 + },
163 },
182 - },
183 - });
164 + });
165 + }
166 }
167
168 return lazyType;
packages/react/src/__tests__/ReactElementClone-test.js
+1
@@ -294,6 +294,7 @@ describe('ReactElementClone', () => {
294 );
295 });
296
297 + // @gate !disableDefaultPropsExceptForClasses
298 it('should normalize props with default values', () => {
299 class Component extends React.Component {
300 render() {
packages/react/src/__tests__/forwardRef-test.js
+1
@@ -74,6 +74,7 @@ describe('forwardRef', () => {
74 expect(ref.current).toBe(null);
75 });
76
77 + // @gate !disableDefaultPropsExceptForClasses
78 it('should support defaultProps', async () => {
79 function FunctionComponent({forwardedRef, optional, required}) {
80 return (
packages/react/src/jsx/ReactJSXElement.js
+31 -15
@@ -18,7 +18,11 @@ import {checkKeyStringCoercion} from 'shared/CheckStringCoercion';
18 import isValidElementType from 'shared/isValidElementType';
19 import isArray from 'shared/isArray';
20 import {describeUnknownElementTypeFrameInDEV} from 'shared/ReactComponentStackFrame';
21 -import {enableRefAsProp, disableStringRefs} from 'shared/ReactFeatureFlags';
21 +import {
22 + enableRefAsProp,
23 + disableStringRefs,
24 + disableDefaultPropsExceptForClasses,
25 +} from 'shared/ReactFeatureFlags';
26
27 const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
28 const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
@@ -340,12 +344,14 @@ export function jsxProd(type, config, maybeKey) {
344 }
345 }
346
343 - // Resolve default props
344 - if (type && type.defaultProps) {
345 - const defaultProps = type.defaultProps;
346 - for (propName in defaultProps) {
347 - if (props[propName] === undefined) {
348 - props[propName] = defaultProps[propName];
347 + if (!disableDefaultPropsExceptForClasses) {
348 + // Resolve default props
349 + if (type && type.defaultProps) {
350 + const defaultProps = type.defaultProps;
351 + for (propName in defaultProps) {
352 + if (props[propName] === undefined) {
353 + props[propName] = defaultProps[propName];
354 + }
355 }
356 }
357 }
@@ -554,12 +560,14 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) {
560 }
561 }
562
557 - // Resolve default props
558 - if (type && type.defaultProps) {
559 - const defaultProps = type.defaultProps;
560 - for (propName in defaultProps) {
561 - if (props[propName] === undefined) {
562 - props[propName] = defaultProps[propName];
563 + if (!disableDefaultPropsExceptForClasses) {
564 + // Resolve default props
565 + if (type && type.defaultProps) {
566 + const defaultProps = type.defaultProps;
567 + for (propName in defaultProps) {
568 + if (props[propName] === undefined) {
569 + props[propName] = defaultProps[propName];
570 + }
571 }
572 }
573 }
@@ -811,7 +819,11 @@ export function cloneElement(element, config, children) {
819
820 // Remaining properties override existing props
821 let defaultProps;
814 - if (element.type && element.type.defaultProps) {
822 + if (
823 + !disableDefaultPropsExceptForClasses &&
824 + element.type &&
825 + element.type.defaultProps
826 + ) {
827 defaultProps = element.type.defaultProps;
828 }
829 for (propName in config) {
@@ -833,7 +845,11 @@ export function cloneElement(element, config, children) {
845 // backwards compatibility.
846 !(enableRefAsProp && propName === 'ref' && config.ref === undefined)
847 ) {
836 - if (config[propName] === undefined && defaultProps !== undefined) {
848 + if (
849 + !disableDefaultPropsExceptForClasses &&
850 + config[propName] === undefined &&
851 + defaultProps !== undefined
852 + ) {
853 // Resolve default props
854 props[propName] = defaultProps[propName];
855 } else {
packages/shared/ReactFeatureFlags.js
+3
@@ -40,6 +40,9 @@ export const disableSchedulerTimeoutInWorkLoop = false;
40 // those can be fixed.
41 export const enableDeferRootSchedulingToMicrotask = true;
42
43 +// TODO: Land at Meta before removing.
44 +export const disableDefaultPropsExceptForClasses = true;
45 +
46 // -----------------------------------------------------------------------------
47 // Slated for removal in the future (significant effort)
48 //
packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js
+1
@@ -27,3 +27,4 @@ export const enableRenderableContext = __VARIANT__;
27 export const enableUnifiedSyncLane = __VARIANT__;
28 export const passChildrenWhenCloningPersistedNodes = __VARIANT__;
29 export const useModernStrictMode = __VARIANT__;
30 +export const disableDefaultPropsExceptForClasses = __VARIANT__;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -29,6 +29,7 @@ export const {
29 enableUnifiedSyncLane,
30 passChildrenWhenCloningPersistedNodes,
31 useModernStrictMode,
32 + disableDefaultPropsExceptForClasses,
33 } = dynamicFlags;
34
35 // The rest of the flags are static for better dead code elimination.
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -37,6 +37,7 @@ export const enableComponentStackLocations = __TODO_NEXT_RN_MAJOR__;
37 // -----------------------------------------------------------------------------
38 export const enableCache = __TODO_NEXT_RN_MAJOR__;
39 export const enableRenderableContext = __TODO_NEXT_RN_MAJOR__;
40 +export const disableDefaultPropsExceptForClasses = __TODO_NEXT_RN_MAJOR__;
41
42 // -----------------------------------------------------------------------------
43 // Already enabled for next React Native major.
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -90,6 +90,7 @@ export const disableLegacyContext = true;
90 export const disableDOMTestUtils = true;
91 export const enableRenderableContext = true;
92 export const enableReactTestRendererWarning = true;
93 +export const disableDefaultPropsExceptForClasses = true;
94
95 // Flow magic to verify the exports of this file match the original version.
96 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+2
@@ -85,5 +85,7 @@ export const enableReactTestRendererWarning = false;
85 export const disableLegacyMode = false;
86 export const disableDOMTestUtils = false;
87
88 +export const disableDefaultPropsExceptForClasses = false;
89 +
90 // Flow magic to verify the exports of this file match the original version.
91 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+2
@@ -85,5 +85,7 @@ export const enableReactTestRendererWarning = false;
85 export const disableLegacyMode = false;
86 export const disableDOMTestUtils = false;
87
88 +export const disableDefaultPropsExceptForClasses = false;
89 +
90 // Flow magic to verify the exports of this file match the original version.
91 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -27,6 +27,7 @@ export const enableRenderableContext = __VARIANT__;
27 export const enableRefAsProp = __VARIANT__;
28 export const enableRetryLaneExpiration = __VARIANT__;
29 export const favorSafetyOverHydrationPerf = __VARIANT__;
30 +export const disableDefaultPropsExceptForClasses = __VARIANT__;
31 export const retryLaneExpirationMs = 5000;
32 export const syncLaneExpirationMs = 250;
33 export const transitionLaneExpirationMs = 5000;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -34,6 +34,7 @@ export const {
34 enableRenderableContext,
35 enableRefAsProp,
36 favorSafetyOverHydrationPerf,
37 + disableDefaultPropsExceptForClasses,
38 } = dynamicFeatureFlags;
39
40 // On WWW, __EXPERIMENTAL__ is used for a new modern build.