Fix to reset the forceUpdate flag after the update (#11440)
* Fix to reset the forceUpdate flag after the update * Add support for PureComponent and mirror real behavior closer
Toru Kobayashi committed
Nov 3, 2017 at 23:29 UTC
e8a382343aac011173a8eb68607e904d7ea26e40
2 files changed
+69
-20
packages/react-test-renderer/src/ReactShallowRenderer.js
+28
-19
@@ -11,6 +11,7 @@ import describeComponentFrame from 'shared/describeComponentFrame';
11
import getComponentName from 'shared/getComponentName';
12
import emptyObject from 'fbjs/lib/emptyObject';
13
import invariant from 'fbjs/lib/invariant';
14
+import shallowEqual from 'fbjs/lib/shallowEqual';
15
import checkPropTypes from 'prop-types/checkPropTypes';
16
17
class ReactShallowRenderer {
@@ -63,7 +64,7 @@ class ReactShallowRenderer {
64
this._context = context;
65
66
if (this._instance) {
66
- this._updateClassComponent(element.props, context);
67
+ this._updateClassComponent(element.type, element.props, context);
68
} else {
69
if (shouldConstruct(element.type)) {
70
this._instance = new element.type(
@@ -133,7 +134,8 @@ class ReactShallowRenderer {
134
// because DOM refs are not available.
135
}
136
136
- _updateClassComponent(props, context) {
137
+ _updateClassComponent(type, props, context) {
138
+ const oldState = this._instance.state || emptyObject;
139
const oldProps = this._instance.props;
140
141
if (
@@ -145,33 +147,40 @@ class ReactShallowRenderer {
147
148
// Read state after cWRP in case it calls setState
149
// Fallback to previous instance state to support rendering React.cloneElement()
150
+ // TODO: the cloneElement() use case is broken and should be removed
151
+ // https://github.com/facebook/react/issues/11441
152
const state = this._newState || this._instance.state || emptyObject;
153
150
- if (
151
- typeof this._instance.shouldComponentUpdate === 'function' &&
152
- !this._forcedUpdate
153
- ) {
154
- if (
155
- this._instance.shouldComponentUpdate(props, state, context) === false
156
- ) {
157
- this._instance.context = context;
158
- this._instance.props = props;
159
- this._instance.state = state;
160
- this._forcedUpdate = false;
161
-
162
- return;
163
- }
154
+ let shouldUpdate = true;
155
+ if (this._forcedUpdate) {
156
+ shouldUpdate = true;
157
+ this._forcedUpdate = false;
158
+ } else if (typeof this._instance.shouldComponentUpdate === 'function') {
159
+ shouldUpdate = !!this._instance.shouldComponentUpdate(
160
+ props,
161
+ state,
162
+ context,
163
+ );
164
+ } else if (type && type.prototype && type.prototype.isPureReactComponent) {
165
+ // TODO: we can remove the type existence check when we fix this:
166
+ // https://github.com/facebook/react/issues/11441
167
+ shouldUpdate =
168
+ !shallowEqual(oldProps, props) || !shallowEqual(oldState, state);
169
}
170
166
- if (typeof this._instance.componentWillUpdate === 'function') {
167
- this._instance.componentWillUpdate(props, state, context);
171
+ if (shouldUpdate) {
172
+ if (typeof this._instance.componentWillUpdate === 'function') {
173
+ this._instance.componentWillUpdate(props, state, context);
174
+ }
175
}
176
177
this._instance.context = context;
178
this._instance.props = props;
179
this._instance.state = state;
180
174
- this._rendered = this._instance.render();
181
+ if (shouldUpdate) {
182
+ this._rendered = this._instance.render();
183
+ }
184
// Intentionally do not call componentDidUpdate()
185
// because DOM refs are not available.
186
}
packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js
+41
-1
@@ -122,24 +122,64 @@ describe('ReactShallowRenderer', () => {
122
expect(shallowRenderer.getRenderOutput()).toEqual(<div>2</div>);
123
});
124
125
+ it('should enable PureComponent to prevent a re-render', () => {
126
+ let renderCounter = 0;
127
+ class SimpleComponent extends React.PureComponent {
128
+ state = {update: false};
129
+ render() {
130
+ renderCounter++;
131
+ return <div>{`${renderCounter}`}</div>;
132
+ }
133
+ }
134
+
135
+ const shallowRenderer = createRenderer();
136
+ shallowRenderer.render(<SimpleComponent />);
137
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>1</div>);
138
+
139
+ const instance = shallowRenderer.getMountedInstance();
140
+ instance.setState({update: false});
141
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>1</div>);
142
+
143
+ instance.setState({update: true});
144
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>2</div>);
145
+ });
146
+
147
it('should not run shouldComponentUpdate during forced update', () => {
148
let scuCounter = 0;
149
class SimpleComponent extends React.Component {
150
+ state = {count: 1};
151
shouldComponentUpdate() {
152
scuCounter++;
153
+ return false;
154
}
155
render() {
132
- return <div />;
156
+ return <div>{`${this.state.count}`}</div>;
157
}
158
}
159
160
const shallowRenderer = createRenderer();
161
shallowRenderer.render(<SimpleComponent />);
162
expect(scuCounter).toEqual(0);
163
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>1</div>);
164
165
+ // Force update the initial state. sCU should not fire.
166
const instance = shallowRenderer.getMountedInstance();
167
instance.forceUpdate();
168
expect(scuCounter).toEqual(0);
169
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>1</div>);
170
+
171
+ // Setting state updates the instance, but doesn't re-render
172
+ // because sCU returned false.
173
+ instance.setState(state => ({count: state.count + 1}));
174
+ expect(scuCounter).toEqual(1);
175
+ expect(instance.state.count).toEqual(2);
176
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>1</div>);
177
+
178
+ // A force update updates the render output, but doesn't call sCU.
179
+ instance.forceUpdate();
180
+ expect(scuCounter).toEqual(1);
181
+ expect(instance.state.count).toEqual(2);
182
+ expect(shallowRenderer.getRenderOutput()).toEqual(<div>2</div>);
183
});
184
185
it('should rerender when calling forceUpdate', () => {