Skip to content

Commit

Permalink
move defaultProps to compat
Browse files Browse the repository at this point in the history
  • Loading branch information
JoviDeCroock committed Apr 29, 2024
1 parent 613cacc commit b1cd110
Show file tree
Hide file tree
Showing 12 changed files with 108 additions and 176 deletions.
11 changes: 11 additions & 0 deletions compat/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ import { JSXInternal } from '../../src/jsx';
import * as _Suspense from './suspense';
import * as _SuspenseList from './suspense-list';

declare module 'preact' {
export namespace preact {
interface FunctionComponent<P = {}> {
defaultProps?: Partial<P>;
}
interface ComponentClass<P = {}, S = {}> {
defaultProps?: Partial<P>;
}
}
}

// export default React;
export = React;
export as namespace React;
Expand Down
9 changes: 9 additions & 0 deletions compat/src/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
useSyncExternalStore,
useTransition
} from './index';
import { assign } from './util';

export const REACT_ELEMENT_TYPE =
(typeof Symbol != 'undefined' && Symbol.for && Symbol.for('react.element')) ||
Expand Down Expand Up @@ -243,6 +244,14 @@ options.vnode = vnode => {
// only normalize props on Element nodes
if (typeof vnode.type === 'string') {
handleDomVNode(vnode);
} else if (typeof vnode.type === 'function' && vnode.type.defaultProps) {
let normalizedProps = assign({}, vnode.props);
for (let i in vnode.type.defaultProps) {
if (normalizedProps[i] === undefined) {
normalizedProps[i] = vnode.type.defaultProps[i];
}
}
vnode.props = normalizedProps;
}

vnode.$$typeof = REACT_ELEMENT_TYPE;
Expand Down
87 changes: 86 additions & 1 deletion compat/test/browser/component.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { setupRerender } from 'preact/test-utils';
import { setupScratch, teardown } from '../../../test/_util/helpers';
import React, { createElement } from 'preact/compat';
import React, { createElement, Component } from 'preact/compat';

describe('components', () => {
/** @type {HTMLDivElement} */
Expand Down Expand Up @@ -240,4 +240,89 @@ describe('components', () => {
expect(Page.prototype.UNSAFE_componentWillMount).to.have.been.called;
});
});

describe('defaultProps', () => {
it('should apply default props on initial render', () => {
class WithDefaultProps extends Component {
constructor(props, context) {
super(props, context);
expect(props).to.be.deep.equal({
fieldA: 1,
fieldB: 2,
fieldC: 1,
fieldD: 2
});
}
render() {
return <div />;
}
}
WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 };
React.render(
<WithDefaultProps fieldA={1} fieldB={2} fieldD={2} />,
scratch
);
});

it('should apply default props on rerender', () => {
let doRender;
class Outer extends Component {
constructor() {
super();
this.state = { i: 1 };
}
componentDidMount() {
doRender = () => this.setState({ i: 2 });
}
render(props, { i }) {
return <WithDefaultProps fieldA={1} fieldB={i} fieldD={i} />;
}
}
class WithDefaultProps extends Component {
constructor(props, context) {
super(props, context);
this.ctor(props, context);
}
ctor() {}
componentWillReceiveProps() {}
render() {
return <div />;
}
}
WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 };

let proto = WithDefaultProps.prototype;
sinon.spy(proto, 'ctor');
sinon.spy(proto, 'componentWillReceiveProps');
sinon.spy(proto, 'render');

React.render(<Outer />, scratch);
doRender();

const PROPS1 = {
fieldA: 1,
fieldB: 1,
fieldC: 1,
fieldD: 1
};

const PROPS2 = {
fieldA: 1,
fieldB: 2,
fieldC: 1,
fieldD: 2
};

expect(proto.ctor).to.have.been.calledWithMatch(PROPS1);
expect(proto.render).to.have.been.calledWithMatch(PROPS1);

rerender();

// expect(proto.ctor).to.have.been.calledWith(PROPS2);
expect(proto.componentWillReceiveProps).to.have.been.calledWithMatch(
PROPS2
);
expect(proto.render).to.have.been.calledWithMatch(PROPS2);
});
});
});
9 changes: 0 additions & 9 deletions jsx-runtime/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,6 @@ function createVNode(type, props, key, isStaticChildren, __source, __self) {
__self
};

// If a Component VNode, check for and apply defaultProps.
// Note: `type` is often a String, and can be `undefined` in development.
if (typeof type === 'function' && (ref = type.defaultProps)) {
for (i in ref)
if (typeof normalizedProps[i] === 'undefined') {
normalizedProps[i] = ref[i];
}
}

if (options.vnode) options.vnode(vnode);
return vnode;
}
Expand Down
34 changes: 0 additions & 34 deletions jsx-runtime/test/browser/jsx-runtime.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,40 +51,6 @@ describe('Babel jsx/jsxDEV', () => {
expect(vnode.key).to.equal('foo');
});

it('should apply defaultProps', () => {
class Foo extends Component {
render() {
return <div />;
}
}

Foo.defaultProps = {
foo: 'bar'
};

const vnode = jsx(Foo, {}, null);
expect(vnode.props).to.deep.equal({
foo: 'bar'
});
});

it('should keep props over defaultProps', () => {
class Foo extends Component {
render() {
return <div />;
}
}

Foo.defaultProps = {
foo: 'bar'
};

const vnode = jsx(Foo, { foo: 'baz' }, null);
expect(vnode.props).to.deep.equal({
foo: 'baz'
});
});

it('should set __source and __self', () => {
const vnode = jsx('div', { class: 'foo' }, 'key', false, 'source', 'self');
expect(vnode.__source).to.equal('source');
Expand Down
10 changes: 1 addition & 9 deletions src/clone-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,10 @@ export function cloneElement(vnode, props, children) {
ref,
i;

let defaultProps;

if (vnode.type && vnode.type.defaultProps) {
defaultProps = vnode.type.defaultProps;
}

for (i in props) {
if (i == 'key') key = props[i];
else if (i == 'ref') ref = props[i];
else if (props[i] === undefined && defaultProps !== undefined) {
normalizedProps[i] = defaultProps[i];
} else {
else {
normalizedProps[i] = props[i];
}
}
Expand Down
10 changes: 0 additions & 10 deletions src/create-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,6 @@ export function createElement(type, props, children) {
arguments.length > 3 ? slice.call(arguments, 2) : children;
}

// If a Component VNode, check for and apply defaultProps
// Note: type may be undefined in development, must never error here.
if (typeof type == 'function' && type.defaultProps != null) {
for (i in type.defaultProps) {
if (normalizedProps[i] === undefined) {
normalizedProps[i] = type.defaultProps[i];
}
}
}

return createVNode(type, normalizedProps, key, ref, null);
}

Expand Down
3 changes: 0 additions & 3 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,12 @@ export type ComponentProps<
export interface FunctionComponent<P = {}> {
(props: RenderableProps<P>, context?: any): VNode<any> | null;
displayName?: string;
defaultProps?: Partial<P> | undefined;
}
export interface FunctionalComponent<P = {}> extends FunctionComponent<P> {}

export interface ComponentClass<P = {}, S = {}> {
new (props: P, context?: any): Component<P, S>;
displayName?: string;
defaultProps?: Partial<P>;
contextType?: Context<any>;
getDerivedStateFromProps?(
props: Readonly<P>,
Expand Down Expand Up @@ -141,7 +139,6 @@ export abstract class Component<P, S> {
constructor(props?: P, context?: any);

static displayName?: string;
static defaultProps?: any;
static contextType?: Context<any>;

// Static members cannot reference class type parameters. This is not
Expand Down
4 changes: 1 addition & 3 deletions src/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,7 @@ declare global {
type Ref<T> = RefObject<T> | RefCallback<T>;

export interface VNode<P = {}> extends preact.VNode<P> {
// Redefine type here using our internal ComponentType type, and specify
// string has an undefined `defaultProps` property to make TS happy
type: (string & { defaultProps: undefined }) | ComponentType<P>;
type: string | ComponentType<P>;
props: P & { children: ComponentChildren };
ref?: Ref<any> | null;
_children: Array<VNode<any>> | null;
Expand Down
13 changes: 0 additions & 13 deletions test/browser/cloneElement.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,4 @@ describe('cloneElement', () => {
const clone = cloneElement(div, { key: 'myKey' });
expect(clone.props.key).to.equal(undefined);
});

it('should prevent undefined properties from overriding default props', () => {
class Example extends Component {
render(props) {
return <div style={{ color: props.color }}>thing</div>;
}
}
Example.defaultProps = { color: 'blue' };

const element = <Example color="red" />;
const clone = cloneElement(element, { color: undefined });
expect(clone.props.color).to.equal('blue');
});
});
82 changes: 0 additions & 82 deletions test/browser/spec.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,88 +16,6 @@ describe('Component spec', () => {
teardown(scratch);
});

describe('defaultProps', () => {
it('should apply default props on initial render', () => {
class WithDefaultProps extends Component {
constructor(props, context) {
super(props, context);
expect(props).to.be.deep.equal({
fieldA: 1,
fieldB: 2,
fieldC: 1,
fieldD: 2
});
}
render() {
return <div />;
}
}
WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 };
render(<WithDefaultProps fieldA={1} fieldB={2} fieldD={2} />, scratch);
});

it('should apply default props on rerender', () => {
let doRender;
class Outer extends Component {
constructor() {
super();
this.state = { i: 1 };
}
componentDidMount() {
doRender = () => this.setState({ i: 2 });
}
render(props, { i }) {
return <WithDefaultProps fieldA={1} fieldB={i} fieldD={i} />;
}
}
class WithDefaultProps extends Component {
constructor(props, context) {
super(props, context);
this.ctor(props, context);
}
ctor() {}
componentWillReceiveProps() {}
render() {
return <div />;
}
}
WithDefaultProps.defaultProps = { fieldC: 1, fieldD: 1 };

let proto = WithDefaultProps.prototype;
sinon.spy(proto, 'ctor');
sinon.spy(proto, 'componentWillReceiveProps');
sinon.spy(proto, 'render');

render(<Outer />, scratch);
doRender();

const PROPS1 = {
fieldA: 1,
fieldB: 1,
fieldC: 1,
fieldD: 1
};

const PROPS2 = {
fieldA: 1,
fieldB: 2,
fieldC: 1,
fieldD: 2
};

expect(proto.ctor).to.have.been.calledWithMatch(PROPS1);
expect(proto.render).to.have.been.calledWithMatch(PROPS1);

rerender();

// expect(proto.ctor).to.have.been.calledWith(PROPS2);
expect(proto.componentWillReceiveProps).to.have.been.calledWithMatch(
PROPS2
);
expect(proto.render).to.have.been.calledWithMatch(PROPS2);
});
});

describe('forceUpdate', () => {
it('should force a rerender', () => {
let forceUpdate;
Expand Down
12 changes: 0 additions & 12 deletions test/shared/createElement.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,18 +265,6 @@ describe('createElement(jsx)', () => {
.that.deep.equals(['x', 'y']);
});

it('should respect defaultProps', () => {
const Component = ({ children }) => children;
Component.defaultProps = { foo: 'bar' };
expect(h(Component).props).to.deep.equal({ foo: 'bar' });
});

it('should override defaultProps', () => {
const Component = ({ children }) => children;
Component.defaultProps = { foo: 'default' };
expect(h(Component, { foo: 'bar' }).props).to.deep.equal({ foo: 'bar' });
});

it('should ignore props.children if children are manually specified', () => {
const element = (
<div a children={['a', 'b']}>
Expand Down

0 comments on commit b1cd110

Please sign in to comment.