Skip to content

Commit cde6123

Browse files
committed
Merge pull request #90 from dallonf/impure-option
Impure option
2 parents 72aa83f + 7a920c2 commit cde6123

File tree

3 files changed

+85
-4
lines changed

3 files changed

+85
-4
lines changed

README.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Performant and flexible.
1515
- [Quick Start](#quick-start)
1616
- [API](#api)
1717
- [`<Provider store>`](#provider-store)
18-
- [`connect([mapStateToProps], [mapDispatchToProps], [mergeProps])`](#connectmapstatetoprops-mapdispatchtoprops-mergeprops)
18+
- [`connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])`](#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options)
1919
- [Troubleshooting](#troubleshooting)
2020
- [License](#license)
2121

@@ -226,7 +226,7 @@ React.render(
226226
);
227227
```
228228

229-
### `connect([mapStateToProps], [mapDispatchToProps], [mergeProps])`
229+
### `connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])`
230230

231231
Connects a React component to a Redux store.
232232

@@ -241,6 +241,10 @@ Instead, it *returns* a new, connected component class, for you to use.
241241

242242
* [`mergeProps(stateProps, dispatchProps, ownProps): 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, `Object.assign({}, ownProps, stateProps, dispatchProps)` is used by default.
243243

244+
* [`options`] *(Object)* If specified, further customizes the behavior of the connector.
245+
246+
* [`pure`] *(Boolean)*: If true, implements `shouldComponentUpdate` and shallowly compares the result of `mergeProps`, preventing unnecessary updates, assuming that the component is a "pure" component and does not rely on any input or state other than its props and the Redux store. *Defaults to `true`.*
247+
244248
#### Returns
245249

246250
A React component class that injects state and action creators into your component according to the specified options.
@@ -460,6 +464,30 @@ render() {
460464
Conveniently, this gives your components access to the router state!
461465
You can also upgrade to React Router 1.0 which shouldn’t have this problem. (Let us know if it does!)
462466

467+
### My views aren't updating when something changes outside of Redux
468+
469+
If your views depend on global state or context, you might find that views decorated with `connect()` will fail to update.
470+
471+
> This is because `connect()` implements [shouldComponentUpdate](https://facebook.github.io/react/docs/component-specs.html#updating-shouldcomponentupdate) by default, assuming that your component will produce the same results given the same props and state. This is a similar concept to React's [PureRenderMixin](https://facebook.github.io/react/docs/pure-render-mixin.html).
472+
473+
The _best_ solution to this is to make sure that your components are pure and pass any external state to them via props. This will ensure that your views do not re-render unless they actually need to re-render and will greatly speed up your application.
474+
475+
If that's not practical for whatever reason (for example, if you're using a library that depends heavily on Context), you can pass the `pure: false` option to `connect()`:
476+
477+
```
478+
function mapStateToProps(state) {
479+
return { todos: state.todos };
480+
}
481+
482+
const options = {
483+
pure: false
484+
};
485+
486+
export default connect(mapStateToProps, null, null, options)(TodoApp);
487+
```
488+
489+
This will remove the assumption that `TodoApp` is pure and cause it to update whenever its parent component renders.
490+
463491
### Could not find "store" in either the context or props
464492

465493
If you have context issues,

src/components/createConnect.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({
1111
...stateProps,
1212
...dispatchProps
1313
});
14+
const defaultOptions = {
15+
pure: true
16+
};
1417

1518
function getDisplayName(Component) {
1619
return Component.displayName || Component.name || 'Component';
@@ -23,7 +26,7 @@ export default function createConnect(React) {
2326
const { Component, PropTypes } = React;
2427
const storeShape = createStoreShape(PropTypes);
2528

26-
return function connect(mapStateToProps, mapDispatchToProps, mergeProps) {
29+
return function connect(mapStateToProps, mapDispatchToProps, mergeProps, options) {
2730
const shouldSubscribe = Boolean(mapStateToProps);
2831
const finalMapStateToProps = mapStateToProps || defaultMapStateToProps;
2932
const finalMapDispatchToProps = isPlainObject(mapDispatchToProps) ?
@@ -32,6 +35,7 @@ export default function createConnect(React) {
3235
const finalMergeProps = mergeProps || defaultMergeProps;
3336
const shouldUpdateStateProps = finalMapStateToProps.length > 1;
3437
const shouldUpdateDispatchProps = finalMapDispatchToProps.length > 1;
38+
const finalOptions = {...defaultOptions, ...options} || defaultOptions;
3539

3640
// Helps track hot reloading.
3741
const version = nextVersion++;
@@ -88,7 +92,7 @@ export default function createConnect(React) {
8892
};
8993

9094
shouldComponentUpdate(nextProps, nextState) {
91-
return !shallowEqual(this.state.props, nextState.props);
95+
return !finalOptions.pure || !shallowEqual(this.state.props, nextState.props);
9296
}
9397

9498
constructor(props, context) {

test/components/connect.spec.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,5 +1064,54 @@ describe('React', () => {
10641064
expect(decorated.getWrappedInstance().someInstanceMethod()).toBe(someData);
10651065
expect(decorated.refs.wrappedInstance.someInstanceMethod()).toBe(someData);
10661066
});
1067+
1068+
it('should wrap impure components without supressing updates', () => {
1069+
const store = createStore(() => ({}));
1070+
1071+
class ImpureComponent extends Component {
1072+
static contextTypes = {
1073+
statefulValue: React.PropTypes.number
1074+
};
1075+
1076+
render() {
1077+
return <Passthrough statefulValue={this.context.statefulValue} />;
1078+
}
1079+
}
1080+
1081+
const decorator = connect(state => state, null, null, { pure: false });
1082+
const Decorated = decorator(ImpureComponent);
1083+
1084+
class StatefulWrapper extends Component {
1085+
state = {
1086+
value: 0
1087+
};
1088+
1089+
static childContextTypes = {
1090+
statefulValue: React.PropTypes.number
1091+
};
1092+
1093+
getChildContext() {
1094+
return {
1095+
statefulValue: this.state.value
1096+
};
1097+
}
1098+
1099+
render() {
1100+
return <Decorated />;
1101+
};
1102+
}
1103+
1104+
const tree = TestUtils.renderIntoDocument(
1105+
<ProviderMock store={store}>
1106+
<StatefulWrapper />
1107+
</ProviderMock>
1108+
);
1109+
1110+
const target = TestUtils.findRenderedComponentWithType(tree, Passthrough);
1111+
const wrapper = TestUtils.findRenderedComponentWithType(tree, StatefulWrapper);
1112+
expect(target.props.statefulValue).toEqual(0);
1113+
wrapper.setState({ value: 1 });
1114+
expect(target.props.statefulValue).toEqual(1);
1115+
});
10671116
});
10681117
});

0 commit comments

Comments
 (0)