diff --git a/src/component.js b/src/component.js index fe4b5e5e3c..9695020eb8 100644 --- a/src/component.js +++ b/src/component.js @@ -53,7 +53,7 @@ Component.prototype.setState = function(update, callback) { const internal = this._internal; if (update != null && internal) { - if (callback) internal._commitCallbacks.push(callback.bind(this)); + if (callback) internal._stateCallbacks.push(callback.bind(this)); internal.rerender(internal); } }; diff --git a/src/diff/mount.js b/src/diff/mount.js index 9e16ba406a..48a84a6e5d 100644 --- a/src/diff/mount.js +++ b/src/diff/mount.js @@ -367,6 +367,10 @@ function mountComponent(internal, startDom) { if (renderHook) renderHook(internal); if (ENABLE_CLASSES && internal.flags & TYPE_CLASS) { renderResult = c.render(c.props, c.state, c.context); + for (let i = 0; i < internal._stateCallbacks.length; i++) { + internal._commitCallbacks.push(internal._stateCallbacks[i]); + } + internal._stateCallbacks = []; // note: disable repeat render invocation for class components break; } else { diff --git a/src/diff/patch.js b/src/diff/patch.js index 545b282509..e21d8f98fb 100644 --- a/src/diff/patch.js +++ b/src/diff/patch.js @@ -244,6 +244,10 @@ function patchComponent(internal, newVNode) { c.props = newProps; c.state = c._nextState; internal.flags |= SKIP_CHILDREN; + for (let i = 0; i < internal._stateCallbacks.length; i++) { + internal._commitCallbacks.push(internal._stateCallbacks[i]); + } + internal._stateCallbacks = []; return; } @@ -265,6 +269,10 @@ function patchComponent(internal, newVNode) { if (renderHook) renderHook(internal); if (ENABLE_CLASSES && internal.flags & TYPE_CLASS) { renderResult = c.render(c.props, c.state, c.context); + for (let i = 0; i < internal._stateCallbacks.length; i++) { + internal._commitCallbacks.push(internal._stateCallbacks[i]); + } + internal._stateCallbacks = []; // note: disable repeat render invocation for class components break; } else { diff --git a/src/internal.d.ts b/src/internal.d.ts index cd186f6c98..30baf77adb 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -164,6 +164,7 @@ export interface Internal

{ _depth: number | null; /** Callbacks to invoke when this internal commits */ _commitCallbacks: Array<() => void>; + _stateCallbacks: Array<() => void>; // Only class components } export interface Component

extends preact.Component { diff --git a/src/tree.js b/src/tree.js index 82a3103e10..1732320ce4 100644 --- a/src/tree.js +++ b/src/tree.js @@ -89,6 +89,7 @@ export function createInternal(vnode, parentInternal) { _prevRef: null, data: flags & TYPE_COMPONENT ? {} : null, _commitCallbacks: flags & TYPE_COMPONENT ? [] : null, + _stateCallbacks: flags & TYPE_COMPONENT ? [] : null, rerender: enqueueRender, flags, _children: null, diff --git a/test/browser/lifecycles/componentDidMount.test.js b/test/browser/lifecycles/componentDidMount.test.js index 086702b4c9..8ca136bfe5 100644 --- a/test/browser/lifecycles/componentDidMount.test.js +++ b/test/browser/lifecycles/componentDidMount.test.js @@ -1,4 +1,5 @@ import { createElement, render, Component } from 'preact'; +import { setupRerender } from 'preact/test-utils'; import { setupScratch, teardown } from '../../_util/helpers'; /** @jsx createElement */ @@ -6,9 +7,11 @@ import { setupScratch, teardown } from '../../_util/helpers'; describe('Lifecycle methods', () => { /** @type {HTMLDivElement} */ let scratch; + let rerender; beforeEach(() => { scratch = setupScratch(); + rerender = setupRerender(); }); afterEach(() => { @@ -32,5 +35,32 @@ describe('Lifecycle methods', () => { render(, scratch); expect(spy).to.have.been.calledOnceWith(scratch.firstChild); }); + + it('supports multiple setState callbacks', () => { + const spy = sinon.spy(); + + class App extends Component { + constructor(props) { + super(props); + this.state = { count: 0 }; + } + + componentDidMount() { + // eslint-disable-next-line + this.setState({ count: 1 }, spy); + // eslint-disable-next-line + this.setState({ count: 2 }, spy); + } + + render() { + return

; + } + } + + render(, scratch); + + rerender(); + expect(spy).to.have.been.calledTwice; + }); }); }); diff --git a/test/browser/lifecycles/componentDidUpdate.test.js b/test/browser/lifecycles/componentDidUpdate.test.js index b897b77ac1..3f6658b283 100644 --- a/test/browser/lifecycles/componentDidUpdate.test.js +++ b/test/browser/lifecycles/componentDidUpdate.test.js @@ -381,5 +381,55 @@ describe('Lifecycle methods', () => { expect(Inner.prototype.componentDidUpdate).to.have.been.called; expect(outerChildText).to.equal(`Outer: ${newValue.toString()}`); }); + + it('should not interfere with setState callbacks', () => { + let invocation; + + class Child extends Component { + componentDidMount() { + this.props.setValue(10); + } + render() { + return

Hello world

; + } + } + + class App extends Component { + constructor(props) { + super(props); + this.state = { + show: false, + count: null + }; + } + + componentDidMount() { + // eslint-disable-next-line + this.setState({ show: true }); + } + + componentDidUpdate() {} + + render() { + if (this.state.show) { + return ( + + this.setState({ count: i }, () => { + invocation = this.state; + }) + } + /> + ); + } + return null; + } + } + + render(, scratch); + + rerender(); + expect(invocation.count).to.equal(10); + }); }); });