Skip to content

Commit

Permalink
Remove defaultProps support (except for classes) (#28733)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed Apr 4, 2024
1 parent 7966ccd commit 48b4ecc
Show file tree
Hide file tree
Showing 24 changed files with 256 additions and 198 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2293,6 +2293,7 @@ describe('ReactHooksInspectionIntegration', () => {
});
});

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

Expand Down
113 changes: 43 additions & 70 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -431,74 +419,59 @@ describe('ReactDOMFizzServer', () => {
});
});

function TextWithPunctuation({text, punctuation}) {
return <Text text={text + punctuation} />;
class TextWithPunctuation extends React.Component {
render() {
return <Text text={this.props.text + this.props.punctuation} />;
}
}

// This tests that default props of the inner element is resolved.
TextWithPunctuation.defaultProps = {
punctuation: '!',
};

try {
await act(() => {
const {pipe} = renderToPipeableStream(
<div>
<div>
<Suspense fallback={<Text text="Loading..." />}>
<LazyA text="Hello" />
</Suspense>
</div>
<div>
<Suspense fallback={<Text text="Loading..." />}>
<LazyB text="world" />
</Suspense>
</div>
</div>,
);
pipe(writable);
});

expect(getVisibleChildren(container)).toEqual(
<div>
<div>Loading...</div>
<div>Loading...</div>
</div>,
);
await act(() => {
resolveA({default: Text});
});
expect(getVisibleChildren(container)).toEqual(
<div>
<div>Hello</div>
<div>Loading...</div>
</div>,
);
await act(() => {
resolveB({default: TextWithPunctuation});
});
expect(getVisibleChildren(container)).toEqual(
await act(() => {
const {pipe} = renderToPipeableStream(
<div>
<div>Hello</div>
<div>world!</div>
<div>
<Suspense fallback={<Text text="Loading..." />}>
<LazyA text="Hello" />
</Suspense>
</div>
<div>
<Suspense fallback={<Text text="Loading..." />}>
<LazyB text="world" />
</Suspense>
</div>
</div>,
);
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(
<div>
<div>Loading...</div>
<div>Loading...</div>
</div>,
);
await act(() => {
resolveA({default: Text});
});
expect(getVisibleChildren(container)).toEqual(
<div>
<div>Hello</div>
<div>Loading...</div>
</div>,
);
await act(() => {
resolveB({default: TextWithPunctuation});
});
expect(getVisibleChildren(container)).toEqual(
<div>
<div>Hello</div>
<div>world!</div>
</div>,
);
});

it('#23331: does not warn about hydration mismatches if something suspended in an earlier sibling', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('ReactDeprecationWarnings', () => {
}
});

// @gate !disableDefaultPropsExceptForClasses || !__DEV__
it('should warn when given defaultProps', async () => {
function FunctionalComponent(props) {
return null;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ describe('ReactFunctionComponent', () => {
);
});

// @gate !disableDefaultPropsExceptForClasses
it('should support default props', async () => {
function Child(props) {
return <div>{props.test}</div>;
Expand All @@ -446,6 +447,7 @@ describe('ReactFunctionComponent', () => {
await act(() => {
root.render(<Child />);
});
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.',
]);
Expand Down
66 changes: 45 additions & 21 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import {
enableRenderableContext,
enableRefAsProp,
disableLegacyMode,
disableDefaultPropsExceptForClasses,
} from 'shared/ReactFeatureFlags';
import isArray from 'shared/isArray';
import shallowEqual from 'shared/shallowEqual';
Expand Down Expand Up @@ -247,7 +248,7 @@ import {
updateClassInstance,
resolveClassComponentProps,
} from './ReactFiberClassComponent';
import {resolveDefaultProps} from './ReactFiberLazyComponent';
import {resolveDefaultPropsOnNonClassComponent} from './ReactFiberLazyComponent';
import {
createFiberFromTypeAndProps,
createFiberFromFragment,
Expand Down Expand Up @@ -487,7 +488,8 @@ function updateMemoComponent(
isSimpleFunctionComponent(type) &&
Component.compare === null &&
// SimpleMemoComponent codepath doesn't resolve outer props either.
Component.defaultProps === undefined
(disableDefaultPropsExceptForClasses ||
Component.defaultProps === undefined)
) {
let resolvedType = type;
if (__DEV__) {
Expand All @@ -509,16 +511,18 @@ function updateMemoComponent(
renderLanes,
);
}
if (__DEV__) {
if (Component.defaultProps !== undefined) {
const componentName = getComponentNameFromType(type) || 'Unknown';
if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) {
console.error(
'%s: Support for defaultProps will be removed from memo components ' +
'in a future major release. Use JavaScript default parameters instead.',
componentName,
);
didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true;
if (!disableDefaultPropsExceptForClasses) {
if (__DEV__) {
if (Component.defaultProps !== undefined) {
const componentName = getComponentNameFromType(type) || 'Unknown';
if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) {
console.error(
'%s: Support for defaultProps will be removed from memo components ' +
'in a future major release. Use JavaScript default parameters instead.',
componentName,
);
didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true;
}
}
}
}
Expand Down Expand Up @@ -1766,7 +1770,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);
Expand All @@ -1784,7 +1790,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 =
Expand All @@ -1798,13 +1806,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,
);
}
Expand Down Expand Up @@ -1900,7 +1915,10 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {
}
}

if (Component.defaultProps !== undefined) {
if (
!disableDefaultPropsExceptForClasses &&
Component.defaultProps !== undefined
) {
const componentName = getComponentNameFromType(Component) || 'Unknown';

if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) {
Expand Down Expand Up @@ -3897,9 +3915,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,
Expand Down Expand Up @@ -3948,9 +3967,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,
Expand All @@ -3973,8 +3993,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,
Expand Down
8 changes: 7 additions & 1 deletion packages/react-reconciler/src/ReactFiberClassComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
enableSchedulingProfiler,
enableLazyContextPropagation,
enableRefAsProp,
disableDefaultPropsExceptForClasses,
} from 'shared/ReactFeatureFlags';
import ReactStrictModeWarnings from './ReactStrictModeWarnings';
import {isMounted} from './ReactFiberTreeReflection';
Expand Down Expand Up @@ -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) {
Expand Down
15 changes: 10 additions & 5 deletions packages/react-reconciler/src/ReactFiberLazyComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 48b4ecc

Please sign in to comment.