Description
I noticed an important slowdown of my application (running in development build but i think it's a good practice to optimize from it) when route change occurred. At first i thought it was some deep bug between the Redux dispatch lifecycle with react-redux
clashing with the one of react-router
since i was dispatching an action at the same time as changing the route but recently i switched to using hooks from the new react-router
API and inspected what they were actually doing, leading me to notice that it just uses useContext
from react
API and extract the right property from it.
The problem is that whenever the react-router
context value is updated on a route change, the useHistory
hook causes a re-render of the component which is using it since the new context value is just a new object containing the newly computed values along with the history
prop.
<RouterContext.Provider
children={this.props.children || null}
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
/>
Since it should not be a problem in most applications, my use case was one where the re-render caused at every route changes were a major performance issue. I use useHistory
(mostly for calling navigation methods) in a lot of components which stay mounted even when a route change since my application is mostly modal-based. This means that at every route change, there is every components using useHistory
which re-render, and even while using memo
in some of them the performance are not that great.
Since the history
prop is not likely to change (or even probably never changes since it's a mutable object, but still not a hundred percent sure) i decided to implement a custom context which will act as a "tank-and-proxy" of the react-router
context for the history
props:
/* I put all the code in one place here for readability purpose */
import * as React from 'react'
import { render } from 'react-dom'
import { useHistory } from 'react-router-dom'
const CustomHistoryContext = React.createContext(null)
function CustomHistoryProvider(props) {
// Tank the updates from react-router context and get the history props
const history = useHistory()
// Provide only the history object
return (
<CustomHistoryContext.Provider value={ history }>
{ props.children }
</CustomHistoryContext.Provider>
)
}
render(
<BrowserRouter>
<CustomHistoryProvider>
<App />
</CustomHistoryProvider>
</BrowserRouter>
, document.getElementById('app'))
and i define my own useHistory
hooks like so:
import * as React from 'react'
import { useContext } from 'react'
import { CustomHistoryContext } from '.../CustomHistoryContext '
export function customUseHistory() {
// Listen from our CustomHistoryContext
return useContext(CustomHistoryContext)
}
And as a test component:
import * as React from 'react'
import { memo, useRef } from 'react'
import { useHistory } from 'react-router-dom'
import { customUseHistory } from '.../customUseHistory'
// This component will render once, and probably never again even when route changes.
const ComponentWhichCustomUseHistory = memo(
props => {
const history = customUseHistory()
const lastHistory = useRef(history)
console.log("#debug customUseHistory test component re-rendered: ", history === lastHistory.current, history)
lastHistory.current = history
return null
}
)
// This component will render at every route changes.
const ComponentWhichUseHistory = memo(
props => {
const history = useHistory()
const lastHistory = useRef(history)
console.log("#debug useHistory test component re-rendered: ", history === lastHistory.current, history)
lastHistory.current = history
return null
}
)
By using my custom useHistory
, performances were back to normal on route changes.
I'm wondering if this would be possible to implement this behavior of "standalone" history context in the current codebase. It's probably not possible from the withRouter
HOC since it involves a lot of things outside of the history
props (and moving the history prop to a withHistory
HOC would break every apps using withRouter
to access the history), but it would be probably a lot easier to do it from the useHistory
hook.
Also i assume that my custom implementation is safe from a lifecycle point of view but it can be a wrong assumption since i'm not that familiar with the react-router
codebase, so correct me if i'm wrong.
Keep up the good work π