diff --git a/src/component.js b/src/component.js index 3c2e442fa0..e3683472bf 100644 --- a/src/component.js +++ b/src/component.js @@ -46,14 +46,16 @@ extend(Component.prototype, { /** * Update component state and schedule a re-render. - * @param {object} state A hash of state properties to update with new values + * @param {object} state A dict of state properties to be shallowly merged + * into the current state, or a function that will produce such a dict. The + * function is called with the current state and props. * @param {() => void} callback A function to be called once component state is * updated */ setState(state, callback) { - let s = this.state; - if (!this.prevState) this.prevState = extend({}, s); - extend(s, typeof state==='function' ? state(s, this.props) : state); + const prev = this.prevState = this.state; + if (typeof state === 'function') state = state(prev, this.props); + this.state = extend(extend({}, prev), state); if (callback) this._renderCallbacks.push(callback); enqueueRender(this); }, diff --git a/src/vdom/component.js b/src/vdom/component.js index 0bd4624405..f788e4e71e 100644 --- a/src/vdom/component.js +++ b/src/vdom/component.js @@ -84,8 +84,8 @@ export function renderComponent(component, renderMode, mountAll, isChild) { rendered, inst, cbase; if (component.constructor.getDerivedStateFromProps) { - previousState = extend({}, previousState); - component.state = extend(state, component.constructor.getDerivedStateFromProps(props, state)); + state = extend(extend({}, state), component.constructor.getDerivedStateFromProps(props, state)); + component.state = state; } // if updating diff --git a/test/browser/lifecycle.js b/test/browser/lifecycle.js index d6b5699363..0b7a6f2d4f 100644 --- a/test/browser/lifecycle.js +++ b/test/browser/lifecycle.js @@ -505,6 +505,31 @@ describe('Lifecycle methods', () => { value: 3 }); }); + + it('should NOT mutate state, only create new versions', () => { + const stateConstant = {}; + let componentState; + + class Stateful extends Component { + static getDerivedStateFromProps() { + return {key: 'value'}; + } + + constructor() { + super(...arguments); + this.state = stateConstant; + } + + componentDidMount() { + componentState = this.state; + } + } + + render(, scratch); + + expect(componentState).to.deep.equal({key: 'value'}); + expect(stateConstant).to.deep.equal({}); + }); }); describe("#getSnapshotBeforeUpdate", () => { @@ -1186,7 +1211,7 @@ describe('Lifecycle methods', () => { }); - describe('shouldComponentUpdate', () => { + describe('#shouldComponentUpdate', () => { let setState; class Should extends Component { @@ -1325,6 +1350,36 @@ describe('Lifecycle methods', () => { }); + describe('#setState', () => { + it('should NOT mutate state, only create new versions', () => { + const stateConstant = {}; + let didMount = false; + let componentState; + + class Stateful extends Component { + constructor() { + super(...arguments); + this.state = stateConstant; + } + + componentDidMount() { + didMount = true; + this.setState({key: 'value'}, () => { + componentState = this.state; + }); + } + } + + render(, scratch); + rerender(); + + expect(didMount).to.equal(true); + expect(componentState).to.deep.equal({key: 'value'}); + expect(stateConstant).to.deep.equal({}); + }); + }), + + describe('Lifecycle DOM Timing', () => { it('should be invoked when dom does (DidMount, WillUnmount) or does not (WillMount, DidUnmount) exist', () => { let setState;