Skip to content

Commit fea5e78

Browse files
committed
Merge pull request #99 from epeli/stale-props
Fix issues with stale props
2 parents d2c33df + 5ed9f40 commit fea5e78

File tree

2 files changed

+127
-28
lines changed

2 files changed

+127
-28
lines changed

src/components/createConnect.js

+29-28
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,30 @@ export default function createConnect(React) {
8989
};
9090

9191
shouldComponentUpdate(nextProps, nextState) {
92-
return !pure || !shallowEqual(this.state.props, nextState.props);
92+
if (!pure) {
93+
this.updateState(nextProps);
94+
return true;
95+
}
96+
97+
const storeChanged = nextState.storeState !== this.state.storeState;
98+
const propsChanged = !shallowEqual(nextProps, this.props);
99+
let mapStateProducedChange = false;
100+
let dispatchPropsChanged = false;
101+
102+
if (storeChanged || (propsChanged && shouldUpdateStateProps)) {
103+
mapStateProducedChange = this.updateStateProps(nextProps);
104+
}
105+
106+
if (propsChanged && shouldUpdateDispatchProps) {
107+
dispatchPropsChanged = this.updateDispatchProps(nextProps);
108+
}
109+
110+
if (propsChanged || mapStateProducedChange || dispatchPropsChanged) {
111+
this.updateState(nextProps);
112+
return true;
113+
}
114+
115+
return false;
93116
}
94117

95118
constructor(props, context) {
@@ -106,9 +129,8 @@ export default function createConnect(React) {
106129

107130
this.stateProps = computeStateProps(this.store, props);
108131
this.dispatchProps = computeDispatchProps(this.store, props);
109-
this.state = {
110-
props: this.computeNextState()
111-
};
132+
this.state = { storeState: null };
133+
this.updateState();
112134
}
113135

114136
computeNextState(props = this.props) {
@@ -140,12 +162,7 @@ export default function createConnect(React) {
140162
}
141163

142164
updateState(props = this.props) {
143-
const nextState = this.computeNextState(props);
144-
if (!shallowEqual(nextState, this.state.props)) {
145-
this.setState({
146-
props: nextState
147-
});
148-
}
165+
this.nextState = this.computeNextState(props);
149166
}
150167

151168
isSubscribed() {
@@ -170,20 +187,6 @@ export default function createConnect(React) {
170187
this.trySubscribe();
171188
}
172189

173-
componentWillReceiveProps(nextProps) {
174-
if (!shallowEqual(nextProps, this.props)) {
175-
if (shouldUpdateStateProps) {
176-
this.updateStateProps(nextProps);
177-
}
178-
179-
if (shouldUpdateDispatchProps) {
180-
this.updateDispatchProps(nextProps);
181-
}
182-
183-
this.updateState(nextProps);
184-
}
185-
}
186-
187190
componentWillUnmount() {
188191
this.tryUnsubscribe();
189192
}
@@ -193,9 +196,7 @@ export default function createConnect(React) {
193196
return;
194197
}
195198

196-
if (this.updateStateProps()) {
197-
this.updateState();
198-
}
199+
this.setState({storeState: this.store.getState()});
199200
}
200201

201202
getWrappedInstance() {
@@ -205,7 +206,7 @@ export default function createConnect(React) {
205206
render() {
206207
return (
207208
<WrappedComponent ref='wrappedInstance'
208-
{...this.state.props} />
209+
{...this.nextState} />
209210
);
210211
}
211212
}

test/components/connect.spec.js

+98
Original file line numberDiff line numberDiff line change
@@ -1147,5 +1147,103 @@ describe('React', () => {
11471147
wrapper.setState({ value: 1 });
11481148
expect(target.props.statefulValue).toEqual(1);
11491149
});
1150+
1151+
it('should pass state consistently to mapState', () => {
1152+
const store = createStore(stringBuilder);
1153+
1154+
store.dispatch({ type: 'APPEND', body: 'a'});
1155+
let childMapStateInvokes = 0;
1156+
1157+
@connect(state => ({ state }))
1158+
class Container extends Component {
1159+
1160+
emitChange() {
1161+
store.dispatch({ type: 'APPEND', body: 'b'});
1162+
}
1163+
1164+
render() {
1165+
return (
1166+
<div>
1167+
<button ref="button" onClick={this.emitChange.bind(this)}>change</button>
1168+
<ChildContainer parentState={this.props.state} />
1169+
</div>
1170+
);
1171+
}
1172+
}
1173+
1174+
@connect((state, parentProps) => {
1175+
childMapStateInvokes++;
1176+
// The state from parent props should always be consistent with the current state
1177+
expect(state).toEqual(parentProps.parentState);
1178+
return {};
1179+
})
1180+
class ChildContainer extends Component {
1181+
render() {
1182+
return <Passthrough {...this.props}/>;
1183+
}
1184+
}
1185+
1186+
const tree = TestUtils.renderIntoDocument(
1187+
<ProviderMock store={store}>
1188+
<Container />
1189+
</ProviderMock>
1190+
);
1191+
1192+
expect(childMapStateInvokes).toBe(2);
1193+
1194+
// The store state stays consistent when setState calls are batched
1195+
ReactDOM.unstable_batchedUpdates(() => {
1196+
store.dispatch({ type: 'APPEND', body: 'c'});
1197+
});
1198+
expect(childMapStateInvokes).toBe(3);
1199+
1200+
// setState calls DOM handlers are batched
1201+
const container = TestUtils.findRenderedComponentWithType(tree, Container);
1202+
const node = React.findDOMNode(container.getWrappedInstance().refs.button);
1203+
TestUtils.Simulate.click(node);
1204+
expect(childMapStateInvokes).toBe(4);
1205+
1206+
// In future all setState calls will be batched[1]. Uncomment when it
1207+
// happens. For now redux-batched-updates middleware can be used as
1208+
// workaround this.
1209+
//
1210+
// [1]: https://twitter.com/sebmarkbage/status/642366976824864768
1211+
//
1212+
// store.dispatch({ type: 'APPEND', body: 'd'});
1213+
// expect(childMapStateInvokes).toBe(5);
1214+
});
1215+
1216+
it('should not render the wrapped component when mapState does not produce change', () => {
1217+
const store = createStore(stringBuilder);
1218+
let renderCalls = 0;
1219+
let mapStateCalls = 0;
1220+
1221+
@connect(() => {
1222+
mapStateCalls++;
1223+
return {}; // no change!
1224+
})
1225+
class Container extends Component {
1226+
render() {
1227+
renderCalls++;
1228+
return <Passthrough {...this.props} />;
1229+
}
1230+
}
1231+
1232+
TestUtils.renderIntoDocument(
1233+
<ProviderMock store={store}>
1234+
<Container />
1235+
</ProviderMock>
1236+
);
1237+
1238+
expect(renderCalls).toBe(1);
1239+
expect(mapStateCalls).toBe(2);
1240+
1241+
store.dispatch({ type: 'APPEND', body: 'a'});
1242+
1243+
// After store a change mapState has been called
1244+
expect(mapStateCalls).toBe(3);
1245+
// But render is not because it did not make any actual changes
1246+
expect(renderCalls).toBe(1);
1247+
});
11501248
});
11511249
});

0 commit comments

Comments
 (0)