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:
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:
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 on GitHub about this behavior. It boils down to the position that it is not a bug, but expected behavior.
Comment for
#7634
useNavigate changes when the current location changes. It depends on it for relative navigation. Wrapping it in memo only prevents re-renders from parent components. If hooks within the component cause re-renders, there is nothing memo can do.
It happens because useNavigate subscribes to contexts that change when path change is triggered (even if it stays the same):
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:
Use the
useNavigatehook in the smallest/lowest-level component possible. It will not save you from re-renders but makes them less costly.Decouple use of the hook from the component, if possible. For example, some of my components can trigger popups and notifications passing to them
navigatefunction. I could move the hook to the popup and notification components themselves, although it would unnecessarily complicate otherwise simple setup."Stabilize" the hook by putting it into a separate context and utilizing a mutable object from the
useRefhook. This is a simplified version of this approach.
// 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. However, since the components will not re-render on the path change anymore, their state may get stale.
Top comments (0)