Skip to content

Commit 48b4ecc

Browse files
authored
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.
1 parent 7966ccd commit 48b4ecc

24 files changed

+256
-198
lines changed

packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2293,6 +2293,7 @@ describe('ReactHooksInspectionIntegration', () => {
22932293
});
22942294
});
22952295

2296+
// @gate !disableDefaultPropsExceptForClasses
22962297
it('should support defaultProps and lazy', async () => {
22972298
const Suspense = React.Suspense;
22982299

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 43 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -405,18 +405,6 @@ describe('ReactDOMFizzServer', () => {
405405
}
406406

407407
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-
420408
let resolveA;
421409
const LazyA = React.lazy(() => {
422410
return new Promise(r => {
@@ -431,74 +419,59 @@ describe('ReactDOMFizzServer', () => {
431419
});
432420
});
433421

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+
}
436426
}
427+
437428
// This tests that default props of the inner element is resolved.
438429
TextWithPunctuation.defaultProps = {
439430
punctuation: '!',
440431
};
441432

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(
480435
<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>
483446
</div>,
484447
);
448+
pipe(writable);
449+
});
485450

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+
);
502475
});
503476

504477
it('#23331: does not warn about hydration mismatches if something suspended in an earlier sibling', async () => {

packages/react-dom/src/__tests__/ReactDeprecationWarnings-test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe('ReactDeprecationWarnings', () => {
2626
}
2727
});
2828

29+
// @gate !disableDefaultPropsExceptForClasses || !__DEV__
2930
it('should warn when given defaultProps', async () => {
3031
function FunctionalComponent(props) {
3132
return null;
@@ -43,6 +44,7 @@ describe('ReactDeprecationWarnings', () => {
4344
);
4445
});
4546

47+
// @gate !disableDefaultPropsExceptForClasses || !__DEV__
4648
it('should warn when given defaultProps on a memoized function', async () => {
4749
const MemoComponent = React.memo(function FunctionalComponent(props) {
4850
return null;

packages/react-dom/src/__tests__/ReactFunctionComponent-test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@ describe('ReactFunctionComponent', () => {
433433
);
434434
});
435435

436+
// @gate !disableDefaultPropsExceptForClasses
436437
it('should support default props', async () => {
437438
function Child(props) {
438439
return <div>{props.test}</div>;
@@ -446,6 +447,7 @@ describe('ReactFunctionComponent', () => {
446447
await act(() => {
447448
root.render(<Child />);
448449
});
450+
expect(container.textContent).toBe('2');
449451
}).toErrorDev([
450452
'Warning: Child: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.',
451453
]);

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ import {
109109
enableRenderableContext,
110110
enableRefAsProp,
111111
disableLegacyMode,
112+
disableDefaultPropsExceptForClasses,
112113
} from 'shared/ReactFeatureFlags';
113114
import isArray from 'shared/isArray';
114115
import shallowEqual from 'shared/shallowEqual';
@@ -247,7 +248,7 @@ import {
247248
updateClassInstance,
248249
resolveClassComponentProps,
249250
} from './ReactFiberClassComponent';
250-
import {resolveDefaultProps} from './ReactFiberLazyComponent';
251+
import {resolveDefaultPropsOnNonClassComponent} from './ReactFiberLazyComponent';
251252
import {
252253
createFiberFromTypeAndProps,
253254
createFiberFromFragment,
@@ -487,7 +488,8 @@ function updateMemoComponent(
487488
isSimpleFunctionComponent(type) &&
488489
Component.compare === null &&
489490
// SimpleMemoComponent codepath doesn't resolve outer props either.
490-
Component.defaultProps === undefined
491+
(disableDefaultPropsExceptForClasses ||
492+
Component.defaultProps === undefined)
491493
) {
492494
let resolvedType = type;
493495
if (__DEV__) {
@@ -509,16 +511,18 @@ function updateMemoComponent(
509511
renderLanes,
510512
);
511513
}
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+
}
522526
}
523527
}
524528
}
@@ -1766,7 +1770,9 @@ function mountLazyComponent(
17661770
renderLanes,
17671771
);
17681772
} else {
1769-
const resolvedProps = resolveDefaultProps(Component, props);
1773+
const resolvedProps = disableDefaultPropsExceptForClasses
1774+
? props
1775+
: resolveDefaultPropsOnNonClassComponent(Component, props);
17701776
workInProgress.tag = FunctionComponent;
17711777
if (__DEV__) {
17721778
validateFunctionComponentInDev(workInProgress, Component);
@@ -1784,7 +1790,9 @@ function mountLazyComponent(
17841790
} else if (Component !== undefined && Component !== null) {
17851791
const $$typeof = Component.$$typeof;
17861792
if ($$typeof === REACT_FORWARD_REF_TYPE) {
1787-
const resolvedProps = resolveDefaultProps(Component, props);
1793+
const resolvedProps = disableDefaultPropsExceptForClasses
1794+
? props
1795+
: resolveDefaultPropsOnNonClassComponent(Component, props);
17881796
workInProgress.tag = ForwardRef;
17891797
if (__DEV__) {
17901798
workInProgress.type = Component =
@@ -1798,13 +1806,20 @@ function mountLazyComponent(
17981806
renderLanes,
17991807
);
18001808
} else if ($$typeof === REACT_MEMO_TYPE) {
1801-
const resolvedProps = resolveDefaultProps(Component, props);
1809+
const resolvedProps = disableDefaultPropsExceptForClasses
1810+
? props
1811+
: resolveDefaultPropsOnNonClassComponent(Component, props);
18021812
workInProgress.tag = MemoComponent;
18031813
return updateMemoComponent(
18041814
null,
18051815
workInProgress,
18061816
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
18081823
renderLanes,
18091824
);
18101825
}
@@ -1900,7 +1915,10 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {
19001915
}
19011916
}
19021917

1903-
if (Component.defaultProps !== undefined) {
1918+
if (
1919+
!disableDefaultPropsExceptForClasses &&
1920+
Component.defaultProps !== undefined
1921+
) {
19041922
const componentName = getComponentNameFromType(Component) || 'Unknown';
19051923

19061924
if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) {
@@ -3897,9 +3915,10 @@ function beginWork(
38973915
const Component = workInProgress.type;
38983916
const unresolvedProps = workInProgress.pendingProps;
38993917
const resolvedProps =
3918+
disableDefaultPropsExceptForClasses ||
39003919
workInProgress.elementType === Component
39013920
? unresolvedProps
3902-
: resolveDefaultProps(Component, unresolvedProps);
3921+
: resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps);
39033922
return updateFunctionComponent(
39043923
current,
39053924
workInProgress,
@@ -3948,9 +3967,10 @@ function beginWork(
39483967
const type = workInProgress.type;
39493968
const unresolvedProps = workInProgress.pendingProps;
39503969
const resolvedProps =
3970+
disableDefaultPropsExceptForClasses ||
39513971
workInProgress.elementType === type
39523972
? unresolvedProps
3953-
: resolveDefaultProps(type, unresolvedProps);
3973+
: resolveDefaultPropsOnNonClassComponent(type, unresolvedProps);
39543974
return updateForwardRef(
39553975
current,
39563976
workInProgress,
@@ -3973,8 +3993,12 @@ function beginWork(
39733993
const type = workInProgress.type;
39743994
const unresolvedProps = workInProgress.pendingProps;
39753995
// 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);
39784002
return updateMemoComponent(
39794003
current,
39804004
workInProgress,

packages/react-reconciler/src/ReactFiberClassComponent.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
enableSchedulingProfiler,
2525
enableLazyContextPropagation,
2626
enableRefAsProp,
27+
disableDefaultPropsExceptForClasses,
2728
} from 'shared/ReactFeatureFlags';
2829
import ReactStrictModeWarnings from './ReactStrictModeWarnings';
2930
import {isMounted} from './ReactFiberTreeReflection';
@@ -1252,7 +1253,12 @@ export function resolveClassComponentProps(
12521253

12531254
// Resolve default props. Taken from old JSX runtime, where this used to live.
12541255
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+
) {
12561262
newProps = assign({}, newProps, baseProps);
12571263
for (const propName in defaultProps) {
12581264
if (newProps[propName] === undefined) {

packages/react-reconciler/src/ReactFiberLazyComponent.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@
88
*/
99

1010
import assign from 'shared/assign';
11+
import {disableDefaultPropsExceptForClasses} from 'shared/ReactFeatureFlags';
1112

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+
}
1722
if (Component && Component.defaultProps) {
1823
// Resolve default props. Taken from ReactElement
1924
const props = assign({}, baseProps);

0 commit comments

Comments
 (0)