Skip to content

Commit 44a5b6c

Browse files
committed
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 `<ClassWithDefaultProp />.props.aDefaultProp` would resolve to the default prop value, but now it does not.
1 parent 9974f1b commit 44a5b6c

File tree

14 files changed

+232
-196
lines changed

14 files changed

+232
-196
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ describe('ReactDeprecationWarnings', () => {
4343
);
4444
});
4545

46+
// @gate !disableDefaultPropsExceptForClasses
4647
it('should warn when given defaultProps on a memoized function', async () => {
4748
const MemoComponent = React.memo(function FunctionalComponent(props) {
4849
return null;

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 41 additions & 20 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,
@@ -488,7 +489,8 @@ function updateMemoComponent(
488489
isSimpleFunctionComponent(type) &&
489490
Component.compare === null &&
490491
// SimpleMemoComponent codepath doesn't resolve outer props either.
491-
Component.defaultProps === undefined
492+
(disableDefaultPropsExceptForClasses ||
493+
Component.defaultProps === undefined)
492494
) {
493495
let resolvedType = type;
494496
if (__DEV__) {
@@ -510,16 +512,18 @@ function updateMemoComponent(
510512
renderLanes,
511513
);
512514
}
513-
if (__DEV__) {
514-
if (Component.defaultProps !== undefined) {
515-
const componentName = getComponentNameFromType(type) || 'Unknown';
516-
if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) {
517-
console.error(
518-
'%s: Support for defaultProps will be removed from memo components ' +
519-
'in a future major release. Use JavaScript default parameters instead.',
520-
componentName,
521-
);
522-
didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true;
515+
if (!disableDefaultPropsExceptForClasses) {
516+
if (__DEV__) {
517+
if (Component.defaultProps !== undefined) {
518+
const componentName = getComponentNameFromType(type) || 'Unknown';
519+
if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) {
520+
console.error(
521+
'%s: Support for defaultProps will be removed from memo components ' +
522+
'in a future major release. Use JavaScript default parameters instead.',
523+
componentName,
524+
);
525+
didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true;
526+
}
523527
}
524528
}
525529
}
@@ -1779,7 +1783,9 @@ function mountLazyComponent(
17791783
renderLanes,
17801784
);
17811785
} else {
1782-
const resolvedProps = resolveDefaultProps(Component, props);
1786+
const resolvedProps = disableDefaultPropsExceptForClasses
1787+
? props
1788+
: resolveDefaultPropsOnNonClassComponent(Component, props);
17831789
workInProgress.tag = FunctionComponent;
17841790
if (__DEV__) {
17851791
validateFunctionComponentInDev(workInProgress, Component);
@@ -1797,7 +1803,9 @@ function mountLazyComponent(
17971803
} else if (Component !== undefined && Component !== null) {
17981804
const $$typeof = Component.$$typeof;
17991805
if ($$typeof === REACT_FORWARD_REF_TYPE) {
1800-
const resolvedProps = resolveDefaultProps(Component, props);
1806+
const resolvedProps = disableDefaultPropsExceptForClasses
1807+
? props
1808+
: resolveDefaultPropsOnNonClassComponent(Component, props);
18011809
workInProgress.tag = ForwardRef;
18021810
if (__DEV__) {
18031811
workInProgress.type = Component =
@@ -1811,13 +1819,20 @@ function mountLazyComponent(
18111819
renderLanes,
18121820
);
18131821
} else if ($$typeof === REACT_MEMO_TYPE) {
1814-
const resolvedProps = resolveDefaultProps(Component, props);
1822+
const resolvedProps = disableDefaultPropsExceptForClasses
1823+
? props
1824+
: resolveDefaultPropsOnNonClassComponent(Component, props);
18151825
workInProgress.tag = MemoComponent;
18161826
return updateMemoComponent(
18171827
null,
18181828
workInProgress,
18191829
Component,
1820-
resolveDefaultProps(Component.type, resolvedProps), // The inner type can have defaults too
1830+
disableDefaultPropsExceptForClasses
1831+
? resolvedProps
1832+
: resolveDefaultPropsOnNonClassComponent(
1833+
Component.type,
1834+
resolvedProps,
1835+
), // The inner type can have defaults too
18211836
renderLanes,
18221837
);
18231838
}
@@ -3928,9 +3943,10 @@ function beginWork(
39283943
const Component = workInProgress.type;
39293944
const unresolvedProps = workInProgress.pendingProps;
39303945
const resolvedProps =
3946+
disableDefaultPropsExceptForClasses ||
39313947
workInProgress.elementType === Component
39323948
? unresolvedProps
3933-
: resolveDefaultProps(Component, unresolvedProps);
3949+
: resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps);
39343950
return updateFunctionComponent(
39353951
current,
39363952
workInProgress,
@@ -3979,9 +3995,10 @@ function beginWork(
39793995
const type = workInProgress.type;
39803996
const unresolvedProps = workInProgress.pendingProps;
39813997
const resolvedProps =
3998+
disableDefaultPropsExceptForClasses ||
39823999
workInProgress.elementType === type
39834000
? unresolvedProps
3984-
: resolveDefaultProps(type, unresolvedProps);
4001+
: resolveDefaultPropsOnNonClassComponent(type, unresolvedProps);
39854002
return updateForwardRef(
39864003
current,
39874004
workInProgress,
@@ -4004,8 +4021,12 @@ function beginWork(
40044021
const type = workInProgress.type;
40054022
const unresolvedProps = workInProgress.pendingProps;
40064023
// Resolve outer props first, then resolve inner props.
4007-
let resolvedProps = resolveDefaultProps(type, unresolvedProps);
4008-
resolvedProps = resolveDefaultProps(type.type, resolvedProps);
4024+
let resolvedProps = disableDefaultPropsExceptForClasses
4025+
? unresolvedProps
4026+
: resolveDefaultPropsOnNonClassComponent(type, unresolvedProps);
4027+
resolvedProps = disableDefaultPropsExceptForClasses
4028+
? resolvedProps
4029+
: resolveDefaultPropsOnNonClassComponent(type.type, resolvedProps);
40094030
return updateMemoComponent(
40104031
current,
40114032
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);

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
alwaysThrottleRetries,
4141
enableInfiniteRenderLoopDetection,
4242
disableLegacyMode,
43+
disableDefaultPropsExceptForClasses,
4344
} from 'shared/ReactFeatureFlags';
4445
import ReactSharedInternals from 'shared/ReactSharedInternals';
4546
import is from 'shared/objectIs';
@@ -264,7 +265,7 @@ import {
264265
getSuspenseHandler,
265266
getShellBoundary,
266267
} from './ReactFiberSuspenseContext';
267-
import {resolveDefaultProps} from './ReactFiberLazyComponent';
268+
import {resolveDefaultPropsOnNonClassComponent} from './ReactFiberLazyComponent';
268269
import {resetChildReconcilerOnUnwind} from './ReactChildFiber';
269270
import {
270271
ensureRootIsScheduled,
@@ -2408,9 +2409,10 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
24082409
const Component = unitOfWork.type;
24092410
const unresolvedProps = unitOfWork.pendingProps;
24102411
const resolvedProps =
2412+
disableDefaultPropsExceptForClasses ||
24112413
unitOfWork.elementType === Component
24122414
? unresolvedProps
2413-
: resolveDefaultProps(Component, unresolvedProps);
2415+
: resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps);
24142416
let context: any;
24152417
if (!disableLegacyContext) {
24162418
const unmaskedContext = getUnmaskedContext(unitOfWork, Component, true);
@@ -2434,9 +2436,10 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
24342436
const Component = unitOfWork.type.render;
24352437
const unresolvedProps = unitOfWork.pendingProps;
24362438
const resolvedProps =
2439+
disableDefaultPropsExceptForClasses ||
24372440
unitOfWork.elementType === Component
24382441
? unresolvedProps
2439-
: resolveDefaultProps(Component, unresolvedProps);
2442+
: resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps);
24402443

24412444
next = replayFunctionComponent(
24422445
current,

0 commit comments

Comments
 (0)