Skip to content

Commit db38380

Browse files
committed
Reinvoke mapState and mapDispatch if a second argument (assumed to be props) is passed in
Fixes #52
1 parent 27aebae commit db38380

File tree

3 files changed

+231
-11
lines changed

3 files changed

+231
-11
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,9 @@ Connects a React component to a Redux store.
226226

227227
#### Arguments
228228

229-
* [`mapStateToProps(state): stateProps`] \(*Function*): If specified, the component will subscribe to Redux store updates. Any time it updates, `mapStateToProps` will be called. Its result must be a plain object, and it will be merged into the component’s props. If you omit it, the component will not be subscribed to the Redux store.
229+
* [`mapStateToProps(state, [props]): stateProps`] \(*Function*): If specified, the component will subscribe to Redux store updates. Any time it updates, `mapStateToProps` will be called. Its result must be a plain object, and it will be merged into the component’s props. If you omit it, the component will not be subscribed to the Redux store. If `props` is passed in as a second argument then `mapStateToProps` will be re-invoked whenever the component receives new props.
230230

231-
* [`mapDispatchToProps(dispatch): dispatchProps`] \(*Object* or *Function*): If an object is passed, each function inside it will be assumed to be a Redux action creator. An object with the same function names, but bound to a Redux store, will be merged into the component’s props. If a function is passed, it will be given `dispatch`. It’s up to you to return an object that somehow uses `dispatch` to bind action creators in your own way. (Tip: you may use [`bindActionCreators()`](http://gaearon.github.io/redux/docs/api/bindActionCreators.html) helper from Redux.) If you omit it, the default implementation just injects `dispatch` into your component’s props.
231+
* [`mapDispatchToProps(dispatch, [props]): dispatchProps`] \(*Object* or *Function*): If an object is passed, each function inside it will be assumed to be a Redux action creator. An object with the same function names, but bound to a Redux store, will be merged into the component’s props. If a function is passed, it will be given `dispatch`. It’s up to you to return an object that somehow uses `dispatch` to bind action creators in your own way. (Tip: you may use [`bindActionCreators()`](http://gaearon.github.io/redux/docs/api/bindActionCreators.html) helper from Redux.) If you omit it, the default implementation just injects `dispatch` into your component’s props. If `props` is passed in as a second argument then `mapStateToProps` will be re-invoked whenever the component receives new props.
232232

233233
* [`mergeProps(stateProps, dispatchProps, parentProps): props`] \(*Function*): If specified, it is passed the result of `mapStateToProps()`, `mapDispatchToProps()`, and the parent `props`. The plain object you return from it will be passed as props to the wrapped component. You may specify this function to select a slice of the state based on props, or to bind action creators to a particular variable from props. If you omit it, `{ ...parentProps, ...stateProps, ...dispatchProps }` is used by default.
234234

src/components/createConnect.js

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,18 @@ export default function createConnect(React) {
3030
wrapActionCreators(mapDispatchToProps) :
3131
mapDispatchToProps || defaultMapDispatchToProps;
3232
const finalMergeProps = mergeProps || defaultMergeProps;
33+
const shouldUpdateStateProps = finalMapStateToProps.length >= 2;
34+
const shouldUpdateDispatchProps = finalMapDispatchToProps.length >= 2;
3335

3436
// Helps track hot reloading.
3537
const version = nextVersion++;
3638

37-
function computeStateProps(store) {
39+
function computeStateProps(store, props) {
3840
const state = store.getState();
39-
const stateProps = finalMapStateToProps(state);
41+
const stateProps = shouldUpdateStateProps ?
42+
finalMapStateToProps(state, props) :
43+
finalMapStateToProps(state);
44+
4045
invariant(
4146
isPlainObject(stateProps),
4247
'`mapStateToProps` must return an object. Instead received %s.',
@@ -45,9 +50,12 @@ export default function createConnect(React) {
4550
return stateProps;
4651
}
4752

48-
function computeDispatchProps(store) {
53+
function computeDispatchProps(store, props) {
4954
const { dispatch } = store;
50-
const dispatchProps = finalMapDispatchToProps(dispatch);
55+
const dispatchProps = shouldUpdateDispatchProps ?
56+
finalMapDispatchToProps(dispatch, props) :
57+
finalMapDispatchToProps(dispatch);
58+
5159
invariant(
5260
isPlainObject(dispatchProps),
5361
'`mapDispatchToProps` must return an object. Instead received %s.',
@@ -95,15 +103,15 @@ export default function createConnect(React) {
95103
`or explicitly pass "store" as a prop to "${this.constructor.displayName}".`
96104
);
97105

98-
this.stateProps = computeStateProps(this.store);
99-
this.dispatchProps = computeDispatchProps(this.store);
106+
this.stateProps = computeStateProps(this.store, props);
107+
this.dispatchProps = computeDispatchProps(this.store, props);
100108
this.state = {
101109
props: this.computeNextState()
102110
};
103111
}
104112

105113
recomputeStateProps() {
106-
const nextStateProps = computeStateProps(this.store);
114+
const nextStateProps = computeStateProps(this.store, this.props);
107115
if (shallowEqual(nextStateProps, this.stateProps)) {
108116
return false;
109117
}
@@ -113,7 +121,7 @@ export default function createConnect(React) {
113121
}
114122

115123
recomputeDispatchProps() {
116-
const nextDispatchProps = computeDispatchProps(this.store);
124+
const nextDispatchProps = computeDispatchProps(this.store, this.props);
117125
if (shallowEqual(nextDispatchProps, this.dispatchProps)) {
118126
return false;
119127
}
@@ -123,6 +131,16 @@ export default function createConnect(React) {
123131
}
124132

125133
computeNextState(props = this.props) {
134+
const propsHaveChanged = !shallowEqual(this.props, props);
135+
136+
if (shouldUpdateStateProps && propsHaveChanged) {
137+
this.stateProps = computeStateProps(this.store, props);
138+
}
139+
140+
if (shouldUpdateDispatchProps && propsHaveChanged) {
141+
this.dispatchProps = computeDispatchProps(this.store, props);
142+
}
143+
126144
return computeNextState(
127145
this.stateProps,
128146
this.dispatchProps,

test/components/connect.spec.js

Lines changed: 203 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ describe('React', () => {
225225
expect('x' in propsAfter).toEqual(false, 'x prop must be removed');
226226
});
227227

228-
it('should remove undefined props without mapDispatchToProps', () => {
228+
it('should remove undefined props without mapDispatch', () => {
229229
const store = createStore(() => ({}));
230230
let props = { x: true };
231231
let container;
@@ -405,6 +405,208 @@ describe('React', () => {
405405
expect(decorated.isSubscribed()).toBe(true);
406406
});
407407

408+
it('should not invoke mapState when props change if it only has one argument', () => {
409+
const store = createStore(stringBuilder);
410+
411+
let invocationCount = 0;
412+
413+
@connect(() => {
414+
invocationCount++;
415+
return {};
416+
})
417+
class WithoutProps extends Component {
418+
render() {
419+
return <div {...this.props}/>;
420+
}
421+
}
422+
423+
class OuterComponent extends Component {
424+
constructor() {
425+
super();
426+
this.state = { foo: 'FOO' };
427+
}
428+
429+
setFoo(foo) {
430+
this.setState({ foo });
431+
}
432+
433+
render() {
434+
return (
435+
<div>
436+
<WithoutProps {...this.state} />
437+
</div>
438+
);
439+
}
440+
}
441+
442+
const tree = TestUtils.renderIntoDocument(
443+
<Provider store={store}>
444+
{() => (
445+
<OuterComponent ref='outerComponent' />
446+
)}
447+
</Provider>
448+
);
449+
450+
tree.refs.outerComponent.setFoo('BAR');
451+
tree.refs.outerComponent.setFoo('DID');
452+
453+
expect(invocationCount).toEqual(2);
454+
});
455+
456+
it('should invoke mapState every time props are changed if it has a second argument', () => {
457+
const store = createStore(stringBuilder);
458+
459+
let propsPassedIn;
460+
let invocationCount = 0;
461+
462+
@connect((state, props) => {
463+
invocationCount++;
464+
propsPassedIn = props;
465+
return {};
466+
})
467+
class WithProps extends Component {
468+
render() {
469+
return <div {...this.props}/>;
470+
}
471+
}
472+
473+
class OuterComponent extends Component {
474+
constructor() {
475+
super();
476+
this.state = { foo: 'FOO' };
477+
}
478+
479+
setFoo(foo) {
480+
this.setState({ foo });
481+
}
482+
483+
render() {
484+
return (
485+
<div>
486+
<WithProps {...this.state} />
487+
</div>
488+
);
489+
}
490+
}
491+
492+
const tree = TestUtils.renderIntoDocument(
493+
<Provider store={store}>
494+
{() => (
495+
<OuterComponent ref='outerComponent' />
496+
)}
497+
</Provider>
498+
);
499+
500+
tree.refs.outerComponent.setFoo('BAR');
501+
tree.refs.outerComponent.setFoo('BAZ');
502+
503+
expect(invocationCount).toEqual(4);
504+
expect(propsPassedIn).toEqual({
505+
foo: 'BAZ'
506+
});
507+
});
508+
509+
it('should not invoke mapDispatch when props change if it only has one argument', () => {
510+
const store = createStore(stringBuilder);
511+
512+
let invocationCount = 0;
513+
514+
@connect(null, () => {
515+
invocationCount++;
516+
return {};
517+
})
518+
class WithoutProps extends Component {
519+
render() {
520+
return <div {...this.props}/>;
521+
}
522+
}
523+
524+
class OuterComponent extends Component {
525+
constructor() {
526+
super();
527+
this.state = { foo: 'FOO' };
528+
}
529+
530+
setFoo(foo) {
531+
this.setState({ foo });
532+
}
533+
534+
render() {
535+
return (
536+
<div>
537+
<WithoutProps {...this.state} />
538+
</div>
539+
);
540+
}
541+
}
542+
543+
const tree = TestUtils.renderIntoDocument(
544+
<Provider store={store}>
545+
{() => (
546+
<OuterComponent ref='outerComponent' />
547+
)}
548+
</Provider>
549+
);
550+
551+
tree.refs.outerComponent.setFoo('BAR');
552+
tree.refs.outerComponent.setFoo('DID');
553+
554+
expect(invocationCount).toEqual(1);
555+
});
556+
557+
it('should invoke mapDispatch every time props are changed if it has a second argument', () => {
558+
const store = createStore(stringBuilder);
559+
560+
let propsPassedIn;
561+
let invocationCount = 0;
562+
563+
@connect(null, (dispatch, props) => {
564+
invocationCount++;
565+
propsPassedIn = props;
566+
return {};
567+
})
568+
class WithProps extends Component {
569+
render() {
570+
return <div {...this.props}/>;
571+
}
572+
}
573+
574+
class OuterComponent extends Component {
575+
constructor() {
576+
super();
577+
this.state = { foo: 'FOO' };
578+
}
579+
580+
setFoo(foo) {
581+
this.setState({ foo });
582+
}
583+
584+
render() {
585+
return (
586+
<div>
587+
<WithProps {...this.state} />
588+
</div>
589+
);
590+
}
591+
}
592+
593+
const tree = TestUtils.renderIntoDocument(
594+
<Provider store={store}>
595+
{() => (
596+
<OuterComponent ref='outerComponent' />
597+
)}
598+
</Provider>
599+
);
600+
601+
tree.refs.outerComponent.setFoo('BAR');
602+
tree.refs.outerComponent.setFoo('BAZ');
603+
604+
expect(invocationCount).toEqual(3);
605+
expect(propsPassedIn).toEqual({
606+
foo: 'BAZ'
607+
});
608+
});
609+
408610
it('should pass dispatch and avoid subscription if arguments are falsy', () => {
409611
const store = createStore(() => ({
410612
foo: 'bar'

0 commit comments

Comments
 (0)