Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initialize compat options when relevant functions are called #2156

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
30 changes: 30 additions & 0 deletions compat/src/Component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Component as PreactComponent } from '../../src/index';
// import { installReactCompat, isReactCompatInstalled } from './render';

// export class Component extends PreactComponent {
// constructor(props, context) {
// super(props, context);
//
// if (!isReactCompatInstalled) {
// installReactCompat();
// }
// }
// }

export function Component() {
PreactComponent.apply(this, arguments);

// if (!isReactCompatInstalled) {
// installReactCompat();
// }

// this.setState = PreactComponent.prototype.setState;
// this.forceUpdate = PreactComponent.prototype.forceUpdate;
// this.render = PreactComponent.prototype.render;
}

// Component.prototype.setState = PreactComponent.prototype.setState;
// Component.prototype.forceUpdate = PreactComponent.prototype.forceUpdate;
// Component.prototype.render = PreactComponent.prototype.render;

// Component.prototype = Object.create(PreactComponent);
41 changes: 29 additions & 12 deletions compat/src/PureComponent.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
import { Component } from 'preact';
import { Component } from './Component';
import { shallowDiffers } from './util';

/**
* Component class with a predefined `shouldComponentUpdate` implementation
*/
export class PureComponent extends Component {
constructor(props) {
super(props);
// Some third-party libraries check if this property is present
this.isPureReactComponent = true;
}
// export class PureComponent extends Component {
// constructor(props) {
// super(props);
// // Some third-party libraries check if this property is present
// this.isPureReactComponent = true;
// }
//
// shouldComponentUpdate(props, state) {
// return (
// shallowDiffers(this.props, props) || shallowDiffers(this.state, state)
// );
// }
// }

shouldComponentUpdate(props, state) {
return (
shallowDiffers(this.props, props) || shallowDiffers(this.state, state)
);
}
export function PureComponent(props, context) {
this.props = props;
this.context = context;

// Some third-party libraries check if this property is present
// this.isReactComponent = {};
// this.isPureReactComponent = true;
}

PureComponent.prototype.isReactComponent = {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a thought: could we use ES5 inheritance here?

export function PureComponent(props, context) {
	// is this smaller?
	//Component.call(this, props, context);
	this.props = props;
	this.context = context;
}
PureComponent.prototype = new Component();
PureComponent.prototype.constructor = PureComponent;
PureComponent.prototype.isReactComponent = {};
PureComponent.prototype.isPureReactComponent = true;
PureComponent.prototype.shouldComponentUpdate = function(props, state) {
	return shallowDiffers(this.props, props) || shallowDiffers(this.state, state);
};

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, nevermind, just saw your explanation.

PureComponent.prototype.isPureReactComponent = true;
PureComponent.prototype.setState = Component.prototype.setState;
PureComponent.prototype.forceUpdate = Component.prototype.forceUpdate;
PureComponent.prototype.shouldComponentUpdate = function(props, state) {
return shallowDiffers(this.props, props) || shallowDiffers(this.state, state);
};
55 changes: 55 additions & 0 deletions compat/src/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Tree shaking in compat TODO
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll remove this file before checking in. Just left it here for discussion


## Classes

### The problem

In order to enable tree-shaking, we need to transform our classes into a tree-shakeable form.

If a class doesn't use inheritance, then the standard prototype assignments should be tree-shakeable. However classes that inherit (or manually inherit using `Suspense.prototype = new Component()`) are not tree-shakeable by default. BabelJS compiles classes into function closures with a `/*#__PURE__*/` comment informing tree-shakers that this closure can be safely removed if unused.

However for Preact, since we bundle our code output, rollup sees these classes as used (because they are exported) and terser minifies the output and removes the comments. This means our classes don't get compiled away if a consumer of Preact doesn't use them because of the lack of a `/*#__PURE__*/` comment.

### Possible solutions

One option is to modify terser to add a new option to keep `/*#__PURE__*/` comments in source (probably pretty difficult...).

Another idea would be to manually code the classes to be tree-shakeable and not use inheritance.

Some code I tried out:

```js
// portals.js
var ContextProvider = function ContextProvider() {};
ContextProvider.prototype.getChildContext = function getChildContext() {
return this.props.context;
};
ContextProvider.prototype.render = function render(props) {
return props.children;
};
Comment on lines +23 to +29
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defining ContextProvider like this saves us 6 bytes

```

```js
// PureComponent.js
// DOESN'T WORK CUZ CONSUMERS NEED TO BE ABLE TO CALL SETSTATE
export function PureComponent() {
this.isPureReactComponent = true;
}
PureComponent.prototype.shouldComponentUpdate = function(props, state) {
return shallowDiffers(this.props, props) || shallowDiffers(this.state, state);
};
```

```js
// suspense.js

// Comment out the following line to remove a side-effect
// Suspense.prototype = new Component();
```

```js
// suspense-list.js

// Comment out the following line to remove a side-effect
// SuspenseList.prototype = new Component();
```
11 changes: 0 additions & 11 deletions compat/src/forwardRef.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
import { options } from 'preact';
import { assign } from './util';

let oldVNodeHook = options.vnode;
options.vnode = vnode => {
if (vnode.type && vnode.type._forwarded && vnode.ref) {
vnode.props.ref = vnode.ref;
vnode.ref = null;
}

if (oldVNodeHook) oldVNodeHook(vnode);
};
Comment on lines -5 to -12
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving this code from compat to core shaves 47 bytes out of compat and only adds 6 bytes to core so I thought it was worth it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And if we didn't install react compat in Component's constructor this entire PR would be free for compat - no bytes added lol. So close!


/**
* Pass ref down to a child. This is mainly used in libraries with HOCs that
* wrap components. Using `forwardRef` there is an easy way to get a reference
Expand Down
11 changes: 7 additions & 4 deletions compat/src/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {
createElement,
render as preactRender,
cloneElement as preactCloneElement,
isValidElement as preactIsValidElement,
createRef,
Component,
createContext,
Fragment
} from 'preact';
Expand All @@ -26,7 +25,8 @@ import { Children } from './Children';
import { Suspense, lazy } from './suspense';
import { SuspenseList } from './suspense-list';
import { createPortal } from './portals';
import { render, REACT_ELEMENT_TYPE } from './render';
import { createElement, render, REACT_ELEMENT_TYPE } from './render';
import { Component } from './Component';

const version = '16.8.0'; // trick libraries to think we are react

Expand All @@ -44,7 +44,10 @@ function createFactory(type) {
* @returns {boolean}
*/
function isValidElement(element) {
return !!element && element.$$typeof === REACT_ELEMENT_TYPE;
return (
(!!element && element.$$typeof === REACT_ELEMENT_TYPE) ||
preactIsValidElement(element)
);
}

/**
Expand Down
156 changes: 88 additions & 68 deletions compat/src/render.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
render as preactRender,
createElement as preactCreateElement,
options,
toChildArray,
Component
Expand All @@ -8,16 +9,21 @@ import { applyEventNormalization } from './events';

const CAMEL_PROPS = /^(?:accent|alignment|arabic|baseline|cap|clip|color|fill|flood|font|glyph|horiz|marker|overline|paint|stop|strikethrough|stroke|text|underline|unicode|units|v|vector|vert|word|writing|x)[A-Z]/;

// Some libraries like `react-virtualized` explicitly check for this.
Component.prototype.isReactComponent = {};

/* istanbul ignore next */
export const REACT_ELEMENT_TYPE =
(typeof Symbol !== 'undefined' &&
Symbol.for &&
Symbol.for('react.element')) ||
0xeac7;

export function createElement() {
if (!isReactCompatInstalled) {
installReactCompat();
}

return preactCreateElement.apply(null, arguments);
}

/**
* Proxy render() since React returns a Component reference.
* @param {import('./internal').VNode} vnode VNode tree to render
Expand All @@ -26,6 +32,10 @@ export const REACT_ELEMENT_TYPE =
* @returns {import('./internal').Component | null} The root component reference or null
*/
export function render(vnode, parent, callback) {
if (!isReactCompatInstalled) {
installReactCompat();
}

// React destroys any existing DOM nodes, see #1727
// ...but only on the first render, see #1828
if (parent._children == null) {
Expand All @@ -40,13 +50,6 @@ export function render(vnode, parent, callback) {
return vnode ? vnode._component : null;
}

let oldEventHook = options.event;
options.event = e => {
if (oldEventHook) e = oldEventHook(e);
e.persist = () => {};
return (e.nativeEvent = e);
};

// Patch in `UNSAFE_*` lifecycle hooks
function setSafeDescriptor(proto, key) {
if (proto['UNSAFE_' + key] && !proto[key]) {
Expand All @@ -66,77 +69,94 @@ function setSafeDescriptor(proto, key) {
}
}

let classNameDescriptor = {
const classNameDescriptor = {
configurable: true,
get() {
return this.class;
}
};

let oldVNodeHook = options.vnode;
options.vnode = vnode => {
vnode.$$typeof = REACT_ELEMENT_TYPE;

let type = vnode.type;
let props = vnode.props;
export let isReactCompatInstalled = false;
export function installReactCompat() {
// Some libraries like `react-virtualized` explicitly check for this.
Component.prototype.isReactComponent = {};

let oldEventHook = options.event;
options.event = e => {
if (oldEventHook) e = oldEventHook(e);
e.persist = () => {};
return (e.nativeEvent = e);
};

let oldVNodeHook = options.vnode;
options.vnode = vnode => {
vnode.$$typeof = REACT_ELEMENT_TYPE;

let type = vnode.type;
let props = vnode.props;

// Apply DOM VNode compat
if (typeof type != 'function') {
// Apply defaultValue to value
if (props.defaultValue) {
if (!props.value && props.value !== 0) {
props.value = props.defaultValue;
}
delete props.defaultValue;
}

// Apply DOM VNode compat
if (typeof type != 'function') {
// Apply defaultValue to value
if (props.defaultValue) {
if (!props.value && props.value !== 0) {
props.value = props.defaultValue;
// Add support for array select values: <select value={[]} />
if (Array.isArray(props.value) && props.multiple && type === 'select') {
toChildArray(props.children).forEach(child => {
if (props.value.indexOf(child.props.value) != -1) {
child.props.selected = true;
}
});
delete props.value;
}
delete props.defaultValue;
}

// Add support for array select values: <select value={[]} />
if (Array.isArray(props.value) && props.multiple && type === 'select') {
toChildArray(props.children).forEach(child => {
if (props.value.indexOf(child.props.value) != -1) {
child.props.selected = true;
// Normalize DOM vnode properties.
let shouldSanitize, attrs, i;
for (i in props) if ((shouldSanitize = CAMEL_PROPS.test(i))) break;
if (shouldSanitize) {
attrs = vnode.props = {};
for (i in props) {
attrs[
CAMEL_PROPS.test(i)
? i.replace(/([A-Z0-9])/, '-$1').toLowerCase()
: i
] = props[i];
}
});
delete props.value;
}
}

// Normalize DOM vnode properties.
let shouldSanitize, attrs, i;
for (i in props) if ((shouldSanitize = CAMEL_PROPS.test(i))) break;
if (shouldSanitize) {
attrs = vnode.props = {};
for (i in props) {
attrs[
CAMEL_PROPS.test(i) ? i.replace(/([A-Z0-9])/, '-$1').toLowerCase() : i
] = props[i];
}
// Alias `class` prop to `className` if available
if (props.class || props.className) {
classNameDescriptor.enumerable = 'className' in props;
if (props.className) props.class = props.className;
Object.defineProperty(props, 'className', classNameDescriptor);
}
}

// Alias `class` prop to `className` if available
if (props.class || props.className) {
classNameDescriptor.enumerable = 'className' in props;
if (props.className) props.class = props.className;
Object.defineProperty(props, 'className', classNameDescriptor);
}
// Events
applyEventNormalization(vnode);

// Component base class compat
// We can't just patch the base component class, because components that use
// inheritance and are transpiled down to ES5 will overwrite our patched
// getters and setters. See #1941
if (
typeof type === 'function' &&
!type._patchedLifecycles &&
type.prototype
) {
setSafeDescriptor(type.prototype, 'componentWillMount');
setSafeDescriptor(type.prototype, 'componentWillReceiveProps');
setSafeDescriptor(type.prototype, 'componentWillUpdate');
type._patchedLifecycles = true;
}

// Events
applyEventNormalization(vnode);

// Component base class compat
// We can't just patch the base component class, because components that use
// inheritance and are transpiled down to ES5 will overwrite our patched
// getters and setters. See #1941
if (
typeof type === 'function' &&
!type._patchedLifecycles &&
type.prototype
) {
setSafeDescriptor(type.prototype, 'componentWillMount');
setSafeDescriptor(type.prototype, 'componentWillReceiveProps');
setSafeDescriptor(type.prototype, 'componentWillUpdate');
type._patchedLifecycles = true;
}
if (oldVNodeHook) oldVNodeHook(vnode);
};

if (oldVNodeHook) oldVNodeHook(vnode);
};
isReactCompatInstalled = true;
}
Loading