Skip to content

Performance concerns on RouterContext with useHistory.Β #6999

Closed
@esealcode

Description

@esealcode

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.

https://github.com/ReactTraining/react-router/blob/ea44618e68f6a112e48404b2ea0da3e207daf4f0/packages/react-router/modules/hooks.js#L17

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 πŸ‘

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions