Skip to content

Commit

Permalink
Merge branch 'master' into drain-in-single-microtask
Browse files Browse the repository at this point in the history
  • Loading branch information
developit committed Aug 17, 2018
2 parents 9806045 + 73866a4 commit 3c17e3e
Show file tree
Hide file tree
Showing 9 changed files with 92 additions and 12 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -97,7 +97,7 @@ Preact supports modern browsers and IE9+:

- [**Preact Boilerplate**](https://preact-boilerplate.surge.sh) _([GitHub Project](https://github.com/developit/preact-boilerplate))_ :zap:
- [**Preact Offline Starter**](https://preact-starter.now.sh) _([GitHub Project](https://github.com/lukeed/preact-starter))_ :100:
- [**Preact PWA**](https://preact-pwa.appspot.com/) _([GitHub Project](https://github.com/ezekielchentnik/preact-pwa))_ :hamburger:
- [**Preact PWA**](https://preact-pwa-yfxiijbzit.now.sh/) _([GitHub Project](https://github.com/ezekielchentnik/preact-pwa))_ :hamburger:
- [**Parcel + Preact + Unistore Starter**](https://github.com/hwclass/parcel-preact-unistore-starter)
- [**Preact Mobx Starter**](https://awaw00.github.io/preact-mobx-starter/) _([GitHub Project](https://github.com/awaw00/preact-mobx-starter))_ :sunny:
- [**Preact Redux Example**](https://github.com/developit/preact-redux-example) :star:
Expand Down
8 changes: 5 additions & 3 deletions src/component.js
Expand Up @@ -53,9 +53,11 @@ extend(Component.prototype, {
* updated
*/
setState(state, callback) {
const prev = this.prevState = this.state;
if (typeof state === 'function') state = state(prev, this.props);
this.state = extend(extend({}, prev), state);
if (!this.prevState) this.prevState = this.state;
this.state = extend(
extend({}, this.state),
typeof state === 'function' ? state(this.state, this.props) : state
);
if (callback) this._renderCallbacks.push(callback);
enqueueRender(this);
},
Expand Down
5 changes: 3 additions & 2 deletions src/dom/index.js
@@ -1,4 +1,5 @@
import { IS_NON_DIMENSIONAL } from '../constants';
import { applyRef } from '../util';
import options from '../options';

/**
Expand Down Expand Up @@ -70,8 +71,8 @@ export function setAccessor(node, name, old, value, isSvg) {
// ignore
}
else if (name==='ref') {
if (old) old(null);
if (value) value(node);
applyRef(old, null);
applyRef(value, node);
}
else if (name==='class' && !isSvg) {
node.className = value || '';
Expand Down
2 changes: 1 addition & 1 deletion src/preact.d.ts
Expand Up @@ -112,7 +112,7 @@ declare namespace preact {
...children: ComponentChildren[]
): VNode<any>;

function render(node: ComponentChild, parent: Element | Document, mergeWith?: Element): Element;
function render(node: ComponentChild, parent: Element | Document | ShadowRoot | DocumentFragment, mergeWith?: Element): Element;
function rerender(): void;
function cloneElement(element: JSX.Element, props: any): JSX.Element;

Expand Down
6 changes: 6 additions & 0 deletions src/preact.js
Expand Up @@ -5,10 +5,15 @@ import { render } from './render';
import { rerender } from './render-queue';
import options from './options';

function createRef() {
return {};
}

export default {
h,
createElement,
cloneElement,
createRef,
Component,
render,
rerender,
Expand All @@ -19,6 +24,7 @@ export {
h,
createElement,
cloneElement,
createRef,
Component,
render,
rerender,
Expand Down
11 changes: 11 additions & 0 deletions src/util.js
Expand Up @@ -10,6 +10,17 @@ export function extend(obj, props) {
return obj;
}

/** Invoke or update a ref, depending on whether it is a function or object ref.
* @param {object|function} [ref=null]
* @param {any} [value]
*/
export function applyRef(ref, value) {
if (ref!=null) {
if (typeof ref=='function') ref(value);
else ref.current = value;
}
}

/**
* Call a function asynchronously, as soon as possible. Makes
* use of HTML Promise to schedule the callback if available,
Expand Down
6 changes: 3 additions & 3 deletions src/vdom/component.js
@@ -1,6 +1,6 @@
import { SYNC_RENDER, NO_RENDER, FORCE_RENDER, ASYNC_RENDER, ATTR_KEY } from '../constants';
import options from '../options';
import { extend } from '../util';
import { extend, applyRef } from '../util';
import { enqueueRender } from '../render-queue';
import { getNodeProps } from './index';
import { diff, mounts, diffLevel, flushMounts, recollectNodeTree, removeChildren } from './diff';
Expand Down Expand Up @@ -52,7 +52,7 @@ export function setComponentProps(component, props, renderMode, context, mountAl
}
}

if (component.__ref) component.__ref(component);
applyRef(component.__ref, component);
}


Expand Down Expand Up @@ -292,5 +292,5 @@ export function unmountComponent(component) {
removeChildren(base);
}

if (component.__ref) component.__ref(null);
applyRef(component.__ref, null);
}
3 changes: 2 additions & 1 deletion src/vdom/diff.js
Expand Up @@ -4,6 +4,7 @@ import { buildComponentFromVNode } from './component';
import { createNode, setAccessor } from '../dom/index';
import { unmountComponent } from './component';
import options from '../options';
import { applyRef } from '../util';
import { removeNode } from '../dom/index';

/**
Expand Down Expand Up @@ -284,7 +285,7 @@ export function recollectNodeTree(node, unmountOnly) {
else {
// If the node's VNode had a ref function, invoke it with null here.
// (this is part of the React spec, and smart for unsetting references)
if (node[ATTR_KEY]!=null && node[ATTR_KEY].ref) node[ATTR_KEY].ref(null);
if (node[ATTR_KEY]!=null) applyRef(node[ATTR_KEY].ref, null);

if (unmountOnly===false || node[ATTR_KEY]==null) {
removeNode(node);
Expand Down
61 changes: 60 additions & 1 deletion test/browser/lifecycle.js
Expand Up @@ -1347,8 +1347,67 @@ describe('Lifecycle methods', () => {
value: 4
});
});
});

it("should be passed correct this.state for batched setState", () => {
/** @type {() => void} */
let updateState;

let curState;
let nextStateArg;
let shouldComponentUpdateCount = 0;

class Foo extends Component {
constructor(props) {
super(props);
this.state = {
value: 0
};
updateState = (value) => this.setState({
value
});
}
shouldComponentUpdate(nextProps, nextState) {
shouldComponentUpdateCount++;
nextStateArg = {...nextState};
curState = {...this.state};
return this.state.value !== nextState.value;
}
render() {
return <div>{this.state.value}</div>;
}
}

// Initial render
// state.value: initialized to 0 in constructor, 0 -> 1 in gDSFP
let element = render(<Foo foo="foo" />, scratch);

expect(element.textContent).to.be.equal('0');
expect(curState).to.be.undefined;
expect(nextStateArg).to.be.undefined;
expect(shouldComponentUpdateCount).to.be.equal(0);

// New state
// state.value: 'bar2'

// batch 2 setState calls with same value
updateState('bar');
updateState('bar2');

// Expectation:
// `this.state` in shouldComponentUpdate should be
// the state from last render, before apply batched setState

rerender();
expect(nextStateArg).to.deep.equal({
value: 'bar2'
});
expect(curState).to.deep.equal({
value: 0
});
expect(shouldComponentUpdateCount).to.be.equal(1);
expect(element.textContent).to.be.equal('bar2');
});
});

describe('#setState', () => {
it('should NOT mutate state, only create new versions', () => {
Expand Down

0 comments on commit 3c17e3e

Please sign in to comment.