[Bug]: useNavigate function triggers constant rerender when used in useEffect

See original GitHub issue

What version of React Router are you using?

6.0.2

Steps to Reproduce

In ReactRouter 6.0.0.beta2 the following code worked and executed only when isAuthenticated changed.

export const AppRouter = () => {
  const { isAuthenticated } = useAuthenticatedUser();
  const navigate = useNavigate();
  const routes = useRoutes(APP_ROUTES);

  useEffect(() => {
    if (isAuthenticated) {
      navigate("Dashboard");
    } else {
      navigate("Login");
    }
  }, [isAuthenticated, navigate]);

  return <Suspense fallback={<LoadingPage />}>{routes}</Suspense>;
};

also this snippet ran useEffect only when activeUser.id changed:

export const Users = () => {
  const { activeUser } = useActiveUser();
  const navigate = useNavigate();

  useEffect(() => {
    if (activeUser.id) {
      navigate(`${activeUser.name}/${activeUser.view}`);
    }

    if (activeUser.id === '') {
      navigate(``);
    }
  }, [navigate, activeUser]);

  return (
    <ContentWithDrawer drawerContent={<UsersNav />}>
      <Suspense fallback={<ContentLoading loading={true} Icon={AccountCircleIcon} withFade={false} />}>
        <Routes>
          <Route path=":name/*" element={<UsersContent />} />
          <Route path="" element={<ContentLoading loading={false} Icon={AccountCircleIcon} />} />
        </Routes>
      </Suspense>
    </ContentWithDrawer>
  );
};

In the current version a re-render is triggered every time navigate() is called somewhere in the App.

This is, I assume, caused by one of the dependencies in the dependecy array of navigate's useCallback. Everytime navigate is called a dependency changes and forces useCallback to create a new function and in the process kill referential equality of navigate which triggers the useEffect. This is just a wild guess because this is one of the main differences that I noticed in a quick overview of useNavigate between 6.0.0-beta.2 vs 6.0.2. I also checked 6.0.0-beta.3. and this behavior exists since then.

Expected Behavior

When navigate function of useNavigate is used as a useEffect dependecy the referential-equality of navigate should be ensured on every route change.

Actual Behavior

The navigate function of useNavigate triggers useEffect on every route change.

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:9
  • Comments:8 (1 by maintainers)

github_iconTop GitHub Comments

3reactions
flexdineshcommented, Dec 23, 2021

I created a small provider that prevents updates from router context for this.

import React from 'react';
import {useNavigate as useNavigateOriginal, useLocation as useLocationOriginal} from 'react-router-dom';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const RouterUtilsContext = React.createContext<any>(null);

/*
  With this RouterUtilsContext - we tank the updates from react-router context and
  drill down navigate and location from a separate context.
  This will prevent re-render of consumer components of these hooks for every route change
  and allow using these hooks as utilities instead of context subscribers
*/
const RouterUtils: React.FC = ({children}) => {
  const navigate = useNavigateOriginal();
  const location = useLocationOriginal();

  // useRef retains object reference between re-renders
  const navigateRef = React.useRef<ReturnType<typeof useNavigateOriginal>>(navigate);
  const locationRef = React.useRef<ReturnType<typeof useLocationOriginal>>(location);

  navigateRef.current = navigate;
  locationRef.current = location;

  // contextValue never changes between re-renders since refs don't change between re-renders
  const contextValue = React.useMemo(() => {
    return {navigateRef, locationRef};
  }, [locationRef, navigateRef]);

  // since contextValue never changes between re-renders, components/hooks using this context
  // won't re-render when router context updates
  return <RouterUtilsContext.Provider value={contextValue}>{children}</RouterUtilsContext.Provider>;
};

/* 

  useNavigate() re-rendering all components is a known bug in react-router
  and might get fixed soon. https://github.com/remix-run/react-router/issues/8349
  Please be aware: when the url changes - this hook will NOT re-render 
  Only use it as a utility to push url changes into Router history
  which will then re-render the whole route component.
  Eg. const navigate = useNavigate();
*/
export const useNavigate = () => {
  const {navigateRef} = React.useContext(RouterUtilsContext);
  return navigateRef.current as ReturnType<typeof useNavigateOriginal>;
};

/* 
  Please be aware: when the url changes - this hook will NOT re-render 
  Only use it as a utility to get latest location object.
  Eg. const location = useLocation();
*/
export const useLocation = () => {
  const {locationRef} = React.useContext(RouterUtilsContext);
  return locationRef.current as ReturnType<typeof useLocationOriginal>;
};

export default RouterUtils;

And in your app — add this provider below Router

  return (
    ...
      <Router>
        <RouterUtils>
          ...
        </RouterUtils>
      </Router>
    ...
  );
2reactions
HansBrendecommented, Jan 16, 2022

@chrishoermann I don’t think devs should have to choose between an “ABSOLUTE” nav style and buggy rendering. Why not simply fix the bug? See my comment on this duplicate issue for a bugfix:

https://github.com/remix-run/react-router/issues/7634#issuecomment-1013806084

Read more comments on GitHub >

github_iconTop Results From Across the Web

Why useNavigate hook in react-router v6 triggers waste re ...
Turns out that if you use the useNavigate hook in a component, it will re-render on every call to navigate() or click on...
Read more >
useEffect runs infinite loop despite no change in dependencies
One simple solution is to simply remove invoiceData from the dependency array. In this way, the useEffect function basically acts similar to ...
Read more >
Preventing infinite re-renders when using useEffect and ...
Changing state will always cause a re-render. By default, useEffect always runs after render has run. This means if you don't include a...
Read more >
useHistory hook - React Router: Declarative Routing for React.js
Please note: You need to be using React >= 16.8 in order to use any of these hooks! ... import { useHistory }...
Read more >
React.useEffect Hook – Common Problems and How to Fix ...
React hooks have been around for a while now. Most developers have gotten pretty comfortable with how they work and their common use...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found