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;