@samitouri / QOS-React-2 / commits / 279b956c9b

Set state takes a function

This diff enables setState to accept a function in addition to a state partial. If you provide a function, it will be called with the up-to-date `state, props, context` as arguments. This enables some nicer syntax for complex setState patterns: If setState is doing an increment and wants to guarantee atomicy, you need a function: ``` this.setState(state => ({ number: state.number + 1 })); ``` This atomicy is particularly important if setState is called multiple times in a single frame of execution as the result of complex user actions. It's a tricky bug to chase down and difficult to determine how to fix when you find it. The current pattern of reaching into _pendingState relies on an implementation detail. In this example: props.doAction() may result in your ancestor re-rendering and providing you with new props. If setState is called directly with an object literal referencing `this.props`, it will use the *old* version of props, not the new value. Using a function solves for this case: ``` this.props.doAction(); this.setState((state, props) => ({ number: state.number * props.multiplier })); ```

Lee Byron committed Jan 30, 2015 at 19:42 UTC 279b956c9b54755163fd73af3b898cee91d2a94f
5 files changed +238 -71
src/core/ReactCompositeComponent.js
+40 -16
@@ -96,7 +96,8 @@ var ReactCompositeComponentMixin = {
96
97 // See ReactUpdateQueue
98 this._pendingElement = null;
99 - this._pendingState = null;
99 + this._pendingStateQueue = null;
100 + this._pendingReplaceState = false;
101 this._pendingForceUpdate = false;
102
103 this._renderedComponent = null;
@@ -191,7 +192,8 @@ var ReactCompositeComponentMixin = {
192 this.getName() || 'ReactCompositeComponent'
193 );
194
194 - this._pendingState = null;
195 + this._pendingStateQueue = null;
196 + this._pendingReplaceState = false;
197 this._pendingForceUpdate = false;
198
199 if (inst.componentWillMount) {
@@ -203,10 +205,9 @@ var ReactCompositeComponentMixin = {
205 ReactLifeCycle.currentlyMountingInstance = previouslyMounting;
206 }
207 // When mounting, calls to `setState` by `componentWillMount` will set
206 - // `this._pendingState` without triggering a re-render.
207 - if (this._pendingState) {
208 - inst.state = this._pendingState;
209 - this._pendingState = null;
208 + // `this._pendingStateQueue` without triggering a re-render.
209 + if (this._pendingStateQueue) {
210 + inst.state = this._processPendingState(inst.props, inst.context);
211 }
212 }
213
@@ -252,7 +253,8 @@ var ReactCompositeComponentMixin = {
253 this._renderedComponent = null;
254
255 // Reset pending fields
255 - this._pendingState = null;
256 + this._pendingStateQueue = null;
257 + this._pendingReplaceState = false;
258 this._pendingForceUpdate = false;
259 this._pendingCallbacks = null;
260 this._pendingElement = null;
@@ -470,7 +472,7 @@ var ReactCompositeComponentMixin = {
472 },
473
474 /**
473 - * If any of `_pendingElement`, `_pendingState`, or `_pendingForceUpdate`
475 + * If any of `_pendingElement`, `_pendingStateQueue`, or `_pendingForceUpdate`
476 * is set, update the component.
477 *
478 * @param {ReactReconcileTransaction} transaction
@@ -486,7 +488,7 @@ var ReactCompositeComponentMixin = {
488 );
489 }
490
489 - if (this._pendingState != null || this._pendingForceUpdate) {
491 + if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
492 if (__DEV__) {
493 ReactElementValidator.checkAndWarnForMutatedProps(
494 this._currentElement
@@ -551,10 +553,8 @@ var ReactCompositeComponentMixin = {
553 ) {
554 var inst = this._instance;
555
554 - var prevContext = inst.context;
555 - var prevProps = inst.props;
556 - var nextContext = prevContext;
557 - var nextProps = prevProps;
556 + var nextContext = inst.context;
557 + var nextProps = inst.props;
558
559 // Distinguish between a props update versus a simple state update
560 if (prevParentElement !== nextParentElement) {
@@ -568,7 +568,7 @@ var ReactCompositeComponentMixin = {
568 }
569
570 // An update here will schedule an update but immediately set
571 - // _pendingState which will ensure that any state updates gets
571 + // _pendingStateQueue which will ensure that any state updates gets
572 // immediately reconciled instead of waiting for the next batch.
573
574 if (inst.componentWillReceiveProps) {
@@ -576,8 +576,7 @@ var ReactCompositeComponentMixin = {
576 }
577 }
578
579 - var nextState = this._pendingState || inst.state;
580 - this._pendingState = null;
579 + var nextState = this._processPendingState(nextProps, nextContext);
580
581 var shouldUpdate =
582 this._pendingForceUpdate ||
@@ -616,6 +615,31 @@ var ReactCompositeComponentMixin = {
615 }
616 },
617
618 + _processPendingState: function(props, context) {
619 + var inst = this._instance;
620 + var queue = this._pendingStateQueue;
621 + var replace = this._pendingReplaceState;
622 + this._pendingReplaceState = false;
623 + this._pendingStateQueue = null;
624 +
625 + if (!queue) {
626 + return inst.state;
627 + }
628 +
629 + var nextState = assign({}, replace ? queue[0] : inst.state);
630 + for (var i = replace ? 1 : 0; i < queue.length; i++) {
631 + var partial = queue[i];
632 + assign(
633 + nextState,
634 + typeof partial === 'function' ?
635 + partial.call(inst, nextState, props, context) :
636 + partial
637 + );
638 + }
639 +
640 + return nextState;
641 + },
642 +
643 /**
644 * Merges new props and state, notifies delegate methods of update and
645 * performs update.
src/core/ReactUpdateQueue.js
+6 -7
@@ -154,7 +154,8 @@ var ReactUpdateQueue = {
154 'replaceState'
155 );
156
157 - internalInstance._pendingState = completeState;
157 + internalInstance._pendingStateQueue = [completeState];
158 + internalInstance._pendingReplaceState = true;
159
160 enqueueUpdate(internalInstance);
161 },
@@ -175,12 +176,10 @@ var ReactUpdateQueue = {
176 'setState'
177 );
178
178 - // Merge with `_pendingState` if it exists, otherwise with existing state.
179 - internalInstance._pendingState = assign(
180 - {},
181 - internalInstance._pendingState || internalInstance._instance.state,
182 - partialState
183 - );
179 + var queue =
180 + internalInstance._pendingStateQueue ||
181 + (internalInstance._pendingStateQueue = []);
182 + queue.push(partialState);
183
184 enqueueUpdate(internalInstance);
185 },
src/core/__tests__/ReactCompositeComponentNestedState-test.js new
+115
@@ -0,0 +1,115 @@
1 +/**
2 + * Copyright 2013-2014, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @emails react-core
10 + */
11 +
12 +'use strict';
13 +
14 +var mocks = require('mocks');
15 +
16 +var React;
17 +var ReactTestUtils;
18 +
19 +describe('ReactCompositeComponentNestedState-state', function() {
20 +
21 + beforeEach(function() {
22 + React = require('React');
23 + ReactTestUtils = require('ReactTestUtils');
24 + });
25 +
26 + it('should provide up to date values for props', function() {
27 + var ParentComponent = React.createClass({
28 + getInitialState: function() {
29 + return { color: 'blue' };
30 + },
31 +
32 + handleColor: function(color) {
33 + this.props.logger('parent-handleColor', this.state.color);
34 + this.setState({ color: color }, function () {
35 + this.props.logger('parent-after-setState', this.state.color);
36 + });
37 + },
38 +
39 + render: function() {
40 + this.props.logger('parent-render', this.state.color);
41 + return <ChildComponent
42 + logger={this.props.logger}
43 + color={this.state.color}
44 + onSelectColor={this.handleColor}
45 + />;
46 + }
47 + });
48 +
49 + var ChildComponent = React.createClass({
50 + getInitialState: function() {
51 + this.props.logger('getInitialState', this.props.color);
52 + return { hue: 'dark ' + this.props.color };
53 + },
54 +
55 + handleHue: function(shade, color) {
56 + this.props.logger('handleHue', this.state.hue, this.props.color);
57 + this.props.onSelectColor(color);
58 + this.setState(function (state, props) {
59 + this.props.logger('setState-this', this.state.hue, this.props.color);
60 + this.props.logger('setState-args', state.hue, props.color);
61 + return { hue: shade + ' ' + props.color }
62 + }, function () {
63 + this.props.logger('after-setState', this.state.hue, this.props.color);
64 + });
65 + },
66 +
67 + render: function() {
68 + this.props.logger('render', this.state.hue, this.props.color);
69 + return <div>
70 + <button onClick={this.handleHue.bind(this, 'dark', 'blue')}>
71 + Dark Blue
72 + </button>
73 + <button onClick={this.handleHue.bind(this, 'light', 'blue')}>
74 + Light Blue
75 + </button>
76 + <button onClick={this.handleHue.bind(this, 'dark', 'green')}>
77 + Dark Green
78 + </button>
79 + <button onClick={this.handleHue.bind(this, 'light', 'green')}>
80 + Light Green
81 + </button>
82 + </div>
83 + }
84 + });
85 +
86 + var container = document.createElement('div');
87 + document.documentElement.appendChild(container);
88 +
89 + var logger = mocks.getMockFunction();
90 +
91 + var instance = React.render(
92 + <ParentComponent logger={logger} />,
93 + container
94 + );
95 +
96 + // click "light green"
97 + ReactTestUtils.Simulate.click(
98 + container.childNodes[0].childNodes[3]
99 + );
100 +
101 + expect(logger.mock.calls).toEqual([
102 + [ 'parent-render', 'blue' ],
103 + [ 'getInitialState', 'blue' ],
104 + [ 'render', 'dark blue', 'blue' ],
105 + [ 'handleHue', 'dark blue', 'blue' ],
106 + [ 'parent-handleColor', 'blue' ],
107 + [ 'parent-render', 'green' ],
108 + [ 'setState-this', 'dark blue', 'blue' ],
109 + [ 'setState-args', 'dark blue', 'green' ],
110 + [ 'render', 'light green', 'green' ],
111 + [ 'parent-after-setState', 'green' ],
112 + [ 'after-setState', 'light green', 'green' ],
113 + ]);
114 + });
115 +});
src/core/__tests__/ReactCompositeComponentState-test.js
+63 -45
@@ -30,18 +30,8 @@ describe('ReactCompositeComponent-state', function() {
30
31 TestComponent = React.createClass({
32 peekAtState: function(from, state) {
33 - if (state) {
34 - this.props.stateListener(from, state && state.color);
35 - } else {
36 - var internalInstance = ReactInstanceMap.get(this);
37 - var pendingState = internalInstance ? internalInstance._pendingState :
38 - null;
39 - this.props.stateListener(
40 - from,
41 - this.state && this.state.color,
42 - pendingState && pendingState.color
43 - );
44 - }
33 + state = state || this.state;
34 + this.props.stateListener(from, state && state.color);
35 },
36
37 peekAtCallback: function(from) {
@@ -67,15 +57,24 @@ describe('ReactCompositeComponent-state', function() {
57
58 componentWillMount: function() {
59 this.peekAtState('componentWillMount-start');
60 + this.setState(function (state) {
61 + this.peekAtState('before-setState-sunrise', state);
62 + });
63 this.setState(
64 {color: 'sunrise'},
65 this.peekAtCallback('setState-sunrise')
66 );
67 + this.setState(function (state) {
68 + this.peekAtState('after-setState-sunrise', state);
69 + });
70 this.peekAtState('componentWillMount-after-sunrise');
71 this.setState(
72 {color: 'orange'},
73 this.peekAtCallback('setState-orange')
74 );
75 + this.setState(function (state) {
76 + this.peekAtState('after-setState-orange', state);
77 + });
78 this.peekAtState('componentWillMount-end');
79 },
80
@@ -91,10 +90,21 @@ describe('ReactCompositeComponent-state', function() {
90 componentWillReceiveProps: function(newProps) {
91 this.peekAtState('componentWillReceiveProps-start');
92 if (newProps.nextColor) {
93 + this.setState(function (state) {
94 + this.peekAtState('before-setState-receiveProps', state);
95 + return {color: newProps.nextColor};
96 + });
97 + this.replaceState({ color: undefined });
98 this.setState(
95 - {color: newProps.nextColor},
99 + function (state) {
100 + this.peekAtState('before-setState-again-receiveProps', state);
101 + return {color: newProps.nextColor};
102 + },
103 this.peekAtCallback('setState-receiveProps')
104 );
105 + this.setState(function (state) {
106 + this.peekAtState('after-setState-receiveProps', state);
107 + });
108 }
109 this.peekAtState('componentWillReceiveProps-end');
110 },
@@ -145,59 +155,67 @@ describe('ReactCompositeComponent-state', function() {
155
156 expect(stateListener.mock.calls).toEqual([
157 // there is no state when getInitialState() is called
148 - [ 'getInitialState', null, null ],
149 - [ 'componentWillMount-start', 'red', null ],
150 - // setState() only enqueues a pending state.
151 - [ 'componentWillMount-after-sunrise', 'red', 'sunrise' ],
152 - [ 'componentWillMount-end', 'red', 'orange' ],
158 + [ 'getInitialState', null ],
159 + [ 'componentWillMount-start', 'red' ],
160 + // setState()'s only enqueue pending states.
161 + [ 'componentWillMount-after-sunrise', 'red' ],
162 + [ 'componentWillMount-end', 'red' ],
163 + // pending state queue is processed
164 + [ 'before-setState-sunrise', 'red' ],
165 + [ 'after-setState-sunrise', 'sunrise' ],
166 + [ 'after-setState-orange', 'orange' ],
167 // pending state has been applied
154 - [ 'render', 'orange', null ],
155 - [ 'componentDidMount-start', 'orange', null ],
168 + [ 'render', 'orange' ],
169 + [ 'componentDidMount-start', 'orange' ],
170 // setState-sunrise and setState-orange should be called here,
171 // after the bug in #1740
172 // componentDidMount() called setState({color:'yellow'}), which is async.
173 // The update doesn't happen until the next flush.
160 - [ 'componentDidMount-end', 'orange', 'yellow' ],
161 - [ 'shouldComponentUpdate-currentState', 'orange', null ],
174 + [ 'componentDidMount-end', 'orange' ],
175 + [ 'shouldComponentUpdate-currentState', 'orange' ],
176 [ 'shouldComponentUpdate-nextState', 'yellow' ],
163 - [ 'componentWillUpdate-currentState', 'orange', null ],
177 + [ 'componentWillUpdate-currentState', 'orange' ],
178 [ 'componentWillUpdate-nextState', 'yellow' ],
165 - [ 'render', 'yellow', null ],
166 - [ 'componentDidUpdate-currentState', 'yellow', null ],
179 + [ 'render', 'yellow' ],
180 + [ 'componentDidUpdate-currentState', 'yellow' ],
181 [ 'componentDidUpdate-prevState', 'orange' ],
168 - [ 'setState-yellow', 'yellow', null ],
169 - [ 'initial-callback', 'yellow', null ],
170 - [ 'componentWillReceiveProps-start', 'yellow', null ],
182 + [ 'setState-yellow', 'yellow' ],
183 + [ 'initial-callback', 'yellow' ],
184 + [ 'componentWillReceiveProps-start', 'yellow' ],
185 // setState({color:'green'}) only enqueues a pending state.
172 - [ 'componentWillReceiveProps-end', 'yellow', 'green' ],
173 - [ 'shouldComponentUpdate-currentState', 'yellow', null ],
186 + [ 'componentWillReceiveProps-end', 'yellow' ],
187 + // pending state queue is processed
188 + // before-setState-receiveProps never called, due to replaceState.
189 + [ 'before-setState-again-receiveProps', undefined ],
190 + [ 'after-setState-receiveProps', 'green' ],
191 + [ 'shouldComponentUpdate-currentState', 'yellow' ],
192 [ 'shouldComponentUpdate-nextState', 'green' ],
175 - [ 'componentWillUpdate-currentState', 'yellow', null ],
193 + [ 'componentWillUpdate-currentState', 'yellow' ],
194 [ 'componentWillUpdate-nextState', 'green' ],
177 - [ 'render', 'green', null ],
178 - [ 'componentDidUpdate-currentState', 'green', null ],
195 + [ 'render', 'green' ],
196 + [ 'componentDidUpdate-currentState', 'green' ],
197 [ 'componentDidUpdate-prevState', 'yellow' ],
180 - [ 'setState-receiveProps', 'green', null ],
181 - [ 'setProps', 'green', null ],
198 + [ 'setState-receiveProps', 'green' ],
199 + [ 'setProps', 'green' ],
200 // setFavoriteColor('blue')
183 - [ 'shouldComponentUpdate-currentState', 'green', null ],
201 + [ 'shouldComponentUpdate-currentState', 'green' ],
202 [ 'shouldComponentUpdate-nextState', 'blue' ],
185 - [ 'componentWillUpdate-currentState', 'green', null ],
203 + [ 'componentWillUpdate-currentState', 'green' ],
204 [ 'componentWillUpdate-nextState', 'blue' ],
187 - [ 'render', 'blue', null ],
188 - [ 'componentDidUpdate-currentState', 'blue', null ],
205 + [ 'render', 'blue' ],
206 + [ 'componentDidUpdate-currentState', 'blue' ],
207 [ 'componentDidUpdate-prevState', 'green' ],
190 - [ 'setFavoriteColor', 'blue', null ],
208 + [ 'setFavoriteColor', 'blue' ],
209 // forceUpdate()
192 - [ 'componentWillUpdate-currentState', 'blue', null ],
210 + [ 'componentWillUpdate-currentState', 'blue' ],
211 [ 'componentWillUpdate-nextState', 'blue' ],
194 - [ 'render', 'blue', null ],
195 - [ 'componentDidUpdate-currentState', 'blue', null ],
212 + [ 'render', 'blue' ],
213 + [ 'componentDidUpdate-currentState', 'blue' ],
214 [ 'componentDidUpdate-prevState', 'blue' ],
197 - [ 'forceUpdate', 'blue', null ],
215 + [ 'forceUpdate', 'blue' ],
216 // unmountComponent()
217 // state is available within `componentWillUnmount()`
200 - [ 'componentWillUnmount', 'blue', null ]
218 + [ 'componentWillUnmount', 'blue' ]
219 ]);
220 });
221 });
src/modern/class/ReactComponent.js
+14 -3
@@ -36,15 +36,26 @@ function ReactComponent(props, context) {
36 * callback that will be executed when the call to setState is actually
37 * completed.
38 *
39 - * @param {object} partialState Next partial state to be merged with state.
39 + * When a function is provided to setState, it will be called at some point in
40 + * the future (not synchronously). It will be called with the up to date
41 + * component arguments (state, props, context). These values can be different
42 + * from this.* because your function may be called after receiveProps but before
43 + * shouldComponentUpdate, and this new state, props, and context will not yet be
44 + * assigned to this.
45 + *
46 + * @param {object|function} partialState Next partial state or function to
47 + * produce next partial state to be merged with current state.
48 * @param {?function} callback Called after state is updated.
49 * @final
50 * @protected
51 */
52 ReactComponent.prototype.setState = function(partialState, callback) {
53 invariant(
46 - typeof partialState === 'object' || partialState == null,
47 - 'setState(...): takes an object of state variables to update.'
54 + typeof partialState === 'object' ||
55 + typeof partialState === 'function' ||
56 + partialState == null,
57 + 'setState(...): takes an object of state variables to update or a ' +
58 + 'function which returns an object of state variables.'
59 );
60 if (__DEV__) {
61 warning(