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
useNavigate
hook 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
navigate
function. 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
useRef
hook. 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)