diff --git a/package.json b/package.json index 2eddc2c8..3044a7e1 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,9 @@ "prop-types": "^15.7.2" }, "peerDependencies": { - "immutable": "^3.8.1 || ^4.0.0-rc.1", "history": "^4.7.2", + "immutable": "^3.8.1 || ^4.0.0-rc.1", + "lodash.isequalwith": "^4.4.0", "react": "^16.4.0", "react-redux": "^6.0.0 || ^7.1.0", "react-router": "^4.3.1 || ^5.0.0", diff --git a/src/ConnectedRouter.js b/src/ConnectedRouter.js index 58d1426b..a2957634 100644 --- a/src/ConnectedRouter.js +++ b/src/ConnectedRouter.js @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import { connect, ReactReduxContext } from 'react-redux' import { Router } from 'react-router' +import isEqualWith from 'lodash.isequalwith' import { onLocationChanged } from './actions' import createSelectors from './selectors' @@ -18,7 +19,7 @@ const createConnectedRouter = (structure) => { constructor(props) { super(props) - const { store, history, onLocationChanged } = props + const { store, history, onLocationChanged, stateCompareFunction } = props this.inTimeTravelling = false @@ -45,7 +46,7 @@ const createConnectedRouter = (structure) => { (pathnameInHistory !== pathnameInStore || searchInHistory !== searchInStore || hashInHistory !== hashInStore || - stateInStore !== stateInHistory) + !isEqualWith(stateInStore, stateInHistory, stateCompareFunction)) ) { this.inTimeTravelling = true // Update history's location to match store's location @@ -109,6 +110,7 @@ const createConnectedRouter = (structure) => { children: PropTypes.oneOfType([ PropTypes.func, PropTypes.node ]), onLocationChanged: PropTypes.func.isRequired, noInitialPop: PropTypes.bool, + stateCompareFunction: PropTypes.func, } const mapDispatchToProps = dispatch => ({ diff --git a/test/ConnectedRouter.test.js b/test/ConnectedRouter.test.js index 9c15ccd5..3deb5103 100644 --- a/test/ConnectedRouter.test.js +++ b/test/ConnectedRouter.test.js @@ -9,7 +9,7 @@ import { createMemoryHistory } from 'history' import { Route } from 'react-router' import { Provider } from 'react-redux' import createConnectedRouter from '../src/ConnectedRouter' -import { onLocationChanged } from '../src/actions' +import { onLocationChanged, LOCATION_CHANGE } from '../src/actions' import plainStructure from '../src/structure/plain' import immutableStructure from '../src/structure/immutable' import seamlessImmutableStructure from '../src/structure/seamless-immutable' @@ -136,6 +136,170 @@ describe('ConnectedRouter', () => { expect(onLocationChangedSpy.mock.calls[1][0].state).toEqual({ foo: 'bar'}) }) + it('updates history when store location state changes', () => { + store = createStore( + combineReducers({ + router: connectRouter(props.history) + }), + compose(applyMiddleware(routerMiddleware(props.history))) + ) + + mount( + + +
Home
} /> +
+
+ ) + + // Need to add PUSH action to history because initial POP action prevents history updates + props.history.push({ pathname: "/" }) + + store.dispatch({ + type: LOCATION_CHANGE, + payload: { + location: { + pathname: '/', + search: '', + hash: '', + state: { foo: 'bar' } + }, + action: 'PUSH', + } + }) + + expect(props.history.entries).toHaveLength(3) + + store.dispatch({ + type: LOCATION_CHANGE, + payload: { + location: { + pathname: '/', + search: '', + hash: '', + state: { foo: 'baz' } + }, + action: 'PUSH', + } + }) + + expect(props.history.entries).toHaveLength(4) + }) + + it('does not update history when store location state is unchanged', () => { + store = createStore( + combineReducers({ + router: connectRouter(props.history) + }), + compose(applyMiddleware(routerMiddleware(props.history))) + ) + + mount( + + +
Home
} /> +
+
+ ) + + // Need to add PUSH action to history because initial POP action prevents history updates + props.history.push({ pathname: "/" }) + + store.dispatch({ + type: LOCATION_CHANGE, + payload: { + location: { + pathname: '/', + search: '', + hash: '', + state: { foo: 'bar' } + }, + action: 'PUSH', + } + }) + + expect(props.history.entries).toHaveLength(3) + + store.dispatch({ + type: LOCATION_CHANGE, + payload: { + location: { + pathname: '/', + search: '', + hash: '', + state: { foo: 'bar' } + }, + action: 'PUSH', + } + }) + + expect(props.history.entries).toHaveLength(3) + }) + + it('supports custom location state compare function', () => { + store = createStore( + combineReducers({ + router: connectRouter(props.history) + }), + compose(applyMiddleware(routerMiddleware(props.history))) + ) + + mount( + + { + // If the store and history states are not undefined, + // prevent history from updating when 'baz' is added to the store after 'bar' + if (storeState !== undefined && historyState !== undefined) { + if (storeState.foo === "baz" && historyState.foo === 'bar') { + return true + } + } + + // Otherwise return a normal object comparison result + return JSON.stringify(storeState) === JSON.stringify(historyState) + }} + {...props} + > +
Home
} /> +
+
+ ) + + // Need to add PUSH action to history because initial POP action prevents history updates + props.history.push({ pathname: "/" }) + + store.dispatch({ + type: LOCATION_CHANGE, + payload: { + location: { + pathname: '/', + search: '', + hash: '', + state: { foo: 'bar' } + }, + action: 'PUSH', + } + }) + + expect(props.history.entries).toHaveLength(3) + + store.dispatch({ + type: LOCATION_CHANGE, + payload: { + location: { + pathname: '/', + search: '', + hash: '', + state: { foo: 'baz' } + }, + action: 'PUSH', + } + }) + + expect(props.history.entries).toHaveLength(3) + }) + it('only renders one time when mounted', () => { let renderCount = 0 diff --git a/yarn.lock b/yarn.lock index 78853479..d889e586 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4733,6 +4733,11 @@ lodash.flattendeep@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= +lodash.isequalwith@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.isequalwith/-/lodash.isequalwith-4.4.0.tgz#266726ddd528f854f21f4ea98a065606e0fbc6b0" + integrity sha1-Jmcm3dUo+FTyH06pigZWBuD7xrA= + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"