Skip to content

Commit

Permalink
options.arePropsEqual for custom prop equality comparison
Browse files Browse the repository at this point in the history
  • Loading branch information
tgriesser committed Nov 11, 2015
1 parent 777652d commit d61d94b
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 7 deletions.
7 changes: 4 additions & 3 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ ReactDOM.render(

Connects a React component to a Redux store.

It does not modify the component class passed to it.
It does not modify the component class passed to it.
Instead, it *returns* a new, connected component class, for you to use.

#### Arguments
Expand All @@ -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

Expand Down Expand Up @@ -104,8 +105,8 @@ export default connect()(TodoApp)

##### Inject `dispatch` and every field in the global state

>Don’t do this! It kills any performance optimizations because `TodoApp` will rerender after every action.
>It’s better to have more granular `connect()` on several components in your view hierarchy that each only
>Don’t do this! It kills any performance optimizations because `TodoApp` will rerender after every action.
>It’s better to have more granular `connect()` on several components in your view hierarchy that each only
>listen to a relevant slice of the state.
```js
Expand Down
8 changes: 4 additions & 4 deletions src/components/connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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++
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
}

Expand All @@ -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
}

Expand Down
52 changes: 52 additions & 0 deletions test/components/connect.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Passthrough {...this.props} />
}
}

TestUtils.renderIntoDocument(
<ProviderMock store={store}>
<Container name="test" />
</ProviderMock>
)

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)
})

})
})

0 comments on commit d61d94b

Please sign in to comment.