Why useNavigate hook in react-router v6 triggers waste re-renders and how to solve it
Tags: react, performance, webdev, javascript
Published: 2022-04-28 15:36:19

While optimizing performance in one of my React.js projects I stumbled upon components re-rendering for no apparent reason whatsoever. After some experiments the culprit was found: 

```js
import { useNavigate } from "react-router-dom"; // v6

...

const Component = () => {
    const navigate = useNavigate();
    ...
}
``` 

Turns out that if you use the `useNavigate` hook in a component, it will re-render on every call to `navigate()` or click on `<Link />`, even if the path has not changed. You cannot prevent it with the `React.memo()`.  

Here is a demonstration:

{% embed https://codesandbox.io/embed/nameless-brook-ujor17?fontsize=14&hidenavigation=1&theme=dark %}

The first block does not call `useNavigate` and is rendered only once. The second uses the hook and is re-rendered twice on every path "change" (I am not clear on why twice, maybe `useNavigate` is to blame again 🤷). The third uses a "stable" version of `useNavigate`, more on that below.

I would say that this is unexpected behavior, especially since `useHistory` in react-router v5 did not cause re-renders. There is a long [discussion](https://github.com/remix-run/react-router/issues/7634) on GitHub about this behavior. It boils down to the position that it is not a bug, but expected behavior.

{% embed https://github.com/remix-run/react-router/issues/7634#issuecomment-700035031 %}

It happens because `useNavigate` [subscribes to contexts](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx#L147) that change when path change is triggered (even if it stays the same):

```js
let { basename, navigator } = React.useContext(NavigationContext);
let { matches } = React.useContext(RouteContext);
let { pathname: locationPathname } = useLocation();
```

Usually, it is not a big problem, because changing the path means changing the view and you need to render a new set of components anyway.  Re-rendering several menu elements is not a problem.

However, when you change parameters in the path without changing the view or there are a lot of constant components that are independent of the path change, it can become painful.

There are several ways to solve this problem:

1. Use the `useNavigate` hook in the smallest/lowest-level component possible. It will not save you from re-renders but makes them less costly.

2. Decouple use of the hook from the component, if possible. For example, some of my components can trigger popups and notifications passing to them `navigate` function.  I could move the hook to the popup and notification components themselves, although it would unnecessarily complicate otherwise simple setup.

3. "Stabilize" the hook by putting it into a separate context and utilizing a mutable object from the `useRef` hook. This is a simplified version of [this approach](https://github.com/remix-run/react-router/issues/7634#issuecomment-1025113528).

```ts
// StableNavigateContext.tsx

import { 
  createContext,
  useContext,
  useRef, 
  MutableRefObject 
} from "react";
import { 
  useNavigate, 
  NavigateFunction 
} from "react-router-dom";

const StableNavigateContext = createContext<MutableRefObject<
  NavigateFunction
> | null>(null);

const StableNavigateContextProvider = ({ children }) => {
  const navigate = useNavigate();
  const navigateRef = useRef(navigate);

  return (
    <StableNavigateContext.Provider value={navigateRef}>
      {children}
    </StableNavigateContext.Provider>
  );
};

const useStableNavigate = (): NavigateFunction => {
  const navigateRef = useContext(StableNavigateContext);
  if (navigateRef.current === null)
    throw new Error("StableNavigate context is not initialized");

  return navigateRef.current;
};

export {
  StableNavigateContext,
  StableNavigateContextProvider,
  useStableNavigate
};


// App.tsx

import { BrowserRouter } from "react-router-dom";
import { 
  StableNavigateContextProvider 
} from "./StableNavigateContext";

export default function App() {
  return (
    <BrowserRouter>
      <StableNavigateContextProvider>
        // ...
      </StableNavigateContextProvider>
    </BrowserRouter>
  );
}


// Component file

import { useStableNavigate } from "./StableNavigateContext";

const Component = () => {
  const navigate = useStableNavigate();
  // ...
};
```

You can use a similar approach for the `useLocation` hook or combine them in one context like in the [original solution](https://github.com/remix-run/react-router/issues/7634#issuecomment-1025113528). However, since the components will not re-render on the path change anymore, their state may get stale.