diff --git a/docs/api.md b/docs/api.md index 4082326be..85561c865 100644 --- a/docs/api.md +++ b/docs/api.md @@ -67,6 +67,7 @@ Instead, it *returns* a new, connected component class, for you to use. * [`options`] *(Object)* If specified, further customizes the behavior of the connector. * [`pure = true`] *(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 selected Redux store’s state. *Defaults to `true`.* * [`withRef = false`] *(Boolean)*: If true, stores a ref to the wrapped component instance and makes it available via `getWrappedInstance()` method. *Defaults to `false`.* + * [`arePropsEqual = shallowEqual`] *(Function)*: Replaces the default equality comparison between props when determining if props have been updated. *Defaults to `shallowEqual` comparison.* #### Returns diff --git a/src/components/connect.js b/src/components/connect.js index 5b31632d1..a93cc679c 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -30,7 +30,7 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, const finalMergeProps = mergeProps || defaultMergeProps const shouldUpdateStateProps = finalMapStateToProps.length > 1 const shouldUpdateDispatchProps = finalMapDispatchToProps.length > 1 - const { pure = true, withRef = false } = options + const { pure = true, withRef = false, arePropsEqual = shallowEqual } = options // Helps track hot reloading. const version = nextVersion++ @@ -84,7 +84,7 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, } const storeChanged = nextState.storeState !== this.state.storeState - const propsChanged = !shallowEqual(nextProps, this.props) + const propsChanged = !arePropsEqual(nextProps, this.props) let mapStateProducedChange = false let dispatchPropsChanged = false @@ -132,7 +132,7 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, updateStateProps(props = this.props) { const nextStateProps = computeStateProps(this.store, props) - if (shallowEqual(nextStateProps, this.stateProps)) { + if (arePropsEqual(nextStateProps, this.stateProps)) { return false } @@ -142,7 +142,7 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, updateDispatchProps(props = this.props) { const nextDispatchProps = computeDispatchProps(this.store, props) - if (shallowEqual(nextDispatchProps, this.dispatchProps)) { + if (arePropsEqual(nextDispatchProps, this.dispatchProps)) { return false } diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index bae556e0a..827277feb 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1369,5 +1369,57 @@ describe('React', () => { // But render is not because it did not make any actual changes expect(renderCalls).toBe(1) }) + + it('should accept arePropsEqual option for custom equality', () => { + const store = createStore(stringBuilder) + let renderCalls = 0 + let mapStateCalls = 0 + + function deeperShallowEqual(maxDepth) { + return function eq(objA, objB, depth = 0) { + if (objA === objB) return true + if (depth > maxDepth) return objA === objB + const keysA = Object.keys(objA) + const keysB = Object.keys(objB) + if (keysA.length !== keysB.length) return false + const hasOwn = Object.prototype.hasOwnProperty + for (let i = 0; i < keysA.length; i++) { + if (!hasOwn.call(objB, keysA[i]) || + !eq(objA[keysA[i]], objB[keysA[i]], depth + 1)) { + return false + } + } + return true + } + } + + @connect((state, props) => { + mapStateCalls++ + return { a: [ 1, 2, 3, 4 ], name: props.name } // no change with new equality comparison! + }, null, null, { arePropsEqual: deeperShallowEqual(1) }) + class Container extends Component { + render() { + renderCalls++ + return + } + } + + TestUtils.renderIntoDocument( + + + + ) + + expect(renderCalls).toBe(1) + expect(mapStateCalls).toBe(2) + + store.dispatch({ type: 'APPEND', body: 'a' }) + + // After store a change mapState has been called + expect(mapStateCalls).toBe(3) + // But render is not because it did not make any actual changes + expect(renderCalls).toBe(1) + }) + }) })