Skip to content

Commit

Permalink
Prevent ConnectedRouter from re-rendering on every redux store update (
Browse files Browse the repository at this point in the history
  • Loading branch information
pmarfany committed Apr 30, 2019
1 parent d442129 commit 1b0663e
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 19 deletions.
33 changes: 15 additions & 18 deletions src/ConnectedRouter.js
@@ -1,32 +1,39 @@
import React, { Component } from 'react'
import React, {PureComponent} from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { Router } from 'react-router'
import { onLocationChanged } from './actions'
import createSelectors from "./selectors"

const createConnectedRouter = (structure) => {
const { getIn, toJS } = structure
const { getLocation } = createSelectors(structure)
/*
* ConnectedRouter listens to a history object passed from props.
* When history is changed, it dispatches action to redux store.
* Then, store will pass props to component to render.
* This creates uni-directional flow from history->store->router->components.
*/

class ConnectedRouter extends Component {
class ConnectedRouter extends PureComponent {

constructor(props, context) {
super(props)
this.passedContext = context
}

componentDidMount() {
const { store, history, onLocationChanged } = this.props
const { history, onLocationChanged } = this.props

this.inTimeTravelling = false

// Subscribe to store changes to check if we are in time travelling
this.unsubscribe = store.subscribe(() => {
// Subscribe to store changes
this.unsubscribe = this.passedContext.store.subscribe(() => {
// Extract store's location
const {
pathname: pathnameInStore,
search: searchInStore,
hash: hashInStore,
} = toJS(getIn(store.getState(), ['router', 'location']))
} = getLocation(this.passedContext.store.getState())
// Extract history's location
const {
pathname: pathnameInHistory,
Expand Down Expand Up @@ -89,26 +96,16 @@ const createConnectedRouter = (structure) => {
location: PropTypes.object.isRequired,
push: PropTypes.func.isRequired,
}).isRequired,
location: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string,
]).isRequired,
action: PropTypes.string.isRequired,
basename: PropTypes.string,
children: PropTypes.oneOfType([ PropTypes.func, PropTypes.node ]),
onLocationChanged: PropTypes.func.isRequired,
}

const mapStateToProps = state => ({
action: getIn(state, ['router', 'action']),
location: getIn(state, ['router', 'location']),
})

const mapDispatchToProps = dispatch => ({
onLocationChanged: (location, action) => dispatch(onLocationChanged(location, action))
})

return connect(mapStateToProps, mapDispatchToProps)(ConnectedRouter)
return connect(null, mapDispatchToProps)(ConnectedRouter)
}

export default createConnectedRouter
171 changes: 170 additions & 1 deletion test/ConnectedRouter.test.js
Expand Up @@ -2,7 +2,7 @@ import 'raf/polyfill'
import React, { Children, Component } from 'react'
import PropTypes from 'prop-types'
import configureStore from 'redux-mock-store'
import { createStore, combineReducers } from 'redux'
import {createStore, combineReducers, compose, applyMiddleware} from 'redux'
import { ActionCreators, instrument } from 'redux-devtools'
import Enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
Expand All @@ -14,6 +14,7 @@ import plainStructure from '../src/structure/plain'
import immutableStructure from '../src/structure/immutable'
import seamlessImmutableStructure from '../src/structure/seamless-immutable'
import { connectRouter, ConnectedRouter } from '../src'
import routerMiddleware from "../src/middleware"

Enzyme.configure({ adapter: new Adapter() })

Expand Down Expand Up @@ -151,6 +152,118 @@ describe('ConnectedRouter', () => {

expect(onLocationChangedSpy.mock.calls).toHaveLength(1)
})

it('only renders one time when mounted', () => {
let renderCount = 0

const RenderCounter = () => {
renderCount++
return null
}

mount(
<ContextWrapper store={store}>
<ConnectedRouter {...props}>
<Route path="/" component={RenderCounter} />
</ConnectedRouter>
</ContextWrapper>
)

expect(renderCount).toBe(1)
})

it('does not render again when non-related action is fired', () => {
// Initialize the render counter variable
let renderCount = 0

// Create redux store with router state
store = createStore(
combineReducers({
incrementReducer: (state = 0, action = {}) => {
if (action.type === 'testAction')
return ++state

return state
},
router: connectRouter(history)
}),
compose(applyMiddleware(routerMiddleware(history)))
)


const RenderCounter = () => {
renderCount++
return null
}

mount(
<ContextWrapper store={store}>
<ConnectedRouter {...props}>
<Route path="/" component={RenderCounter} />
</ConnectedRouter>
</ContextWrapper>
)

store.dispatch({ type: 'testAction' })
history.push('/new-location')
expect(renderCount).toBe(2)
})

it('only renders one time when mounted', () => {
let renderCount = 0

const RenderCounter = () => {
renderCount++
return null
}

mount(
<ContextWrapper store={store}>
<ConnectedRouter {...props}>
<Route path="/" component={RenderCounter} />
</ConnectedRouter>
</ContextWrapper>
)

expect(renderCount).toBe(1)
})

it('does not render again when non-related action is fired', () => {
// Initialize the render counter variable
let renderCount = 0

// Create redux store with router state
store = createStore(
combineReducers({
incrementReducer: (state = 0, action = {}) => {
if (action.type === 'testAction')
return ++state

return state
},
router: connectRouter(history)
}),
compose(applyMiddleware(routerMiddleware(history)))
)


const RenderCounter = () => {
renderCount++
return null
}

mount(
<ContextWrapper store={store}>
<ConnectedRouter {...props}>
<Route path="/" component={RenderCounter} />
</ConnectedRouter>
</ContextWrapper>
)

store.dispatch({ type: 'testAction' })
history.push('/new-location')
expect(renderCount).toBe(2)
})
})

describe('with seamless immutable structure', () => {
Expand Down Expand Up @@ -198,6 +311,62 @@ describe('ConnectedRouter', () => {

expect(onLocationChangedSpy.mock.calls).toHaveLength(1)
})

it('only renders one time when mounted', () => {
let renderCount = 0

const RenderCounter = () => {
renderCount++
return null
}

mount(
<ContextWrapper store={store}>
<ConnectedRouter {...props}>
<Route path="/" component={RenderCounter} />
</ConnectedRouter>
</ContextWrapper>
)

expect(renderCount).toBe(1)
})

it('does not render again when non-related action is fired', () => {
// Initialize the render counter variable
let renderCount = 0

// Create redux store with router state
store = createStore(
combineReducers({
incrementReducer: (state = 0, action = {}) => {
if (action.type === 'testAction')
return ++state

return state
},
router: connectRouter(history)
}),
compose(applyMiddleware(routerMiddleware(history)))
)


const RenderCounter = () => {
renderCount++
return null
}

mount(
<ContextWrapper store={store}>
<ConnectedRouter {...props}>
<Route path="/" component={RenderCounter} />
</ConnectedRouter>
</ContextWrapper>
)

store.dispatch({ type: 'testAction' })
history.push('/new-location')
expect(renderCount).toBe(2)
})
})

describe('Redux DevTools', () => {
Expand Down

0 comments on commit 1b0663e

Please sign in to comment.