@samitouri / QOS-React / commits / e8a382343a

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', () => {