Search code examples
javascriptreactjsreact-router-dom

useNavigation changes URL but not does not render or display component


Consider the following code:

https://codesandbox.io/p/sandbox/hardcore-lake-mptzw3

App.jsx:

import ContextProvider from "./provider/contextProvider";
import Routes from "./routes";
function App() {
  console.log("In App");

  return (
    <ContextProvider>
      <Routes />
    </ContextProvider>
  );
}

export default App;

Routes.jsx:

import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { provideContext } from "./provider/contextProvider";
import Component1 from "./pages/Component1";
import Component2 from "./pages/Component2";
const Routes = () => {
  const { token } = provideContext();

  const router = createBrowserRouter([
    {
      path: "/Component2",
      element: <Component2 />,
    },
    {
      path: "/Component1",
      element: <Component1 />,
    },
  ]);


  return <RouterProvider router={router} />;
};

export default Routes;

pages/Component1.jsx:

const Component1 = () => {
  console.log("In Component1");

  return <div>Component1</div>;
};

export default Component1;

pages/Component2.jsx:

import { useNavigate } from "react-router-dom";
import { provideContext } from "../provider/contextProvider";

const Component2 = () => {
  const navigate = useNavigate();
  const { setToken } = provideContext();
  console.log("In Component2");
  const onClick = () => {
    setToken("Token");
    setTimeout(() => {
      navigate("/Component1");
    });
  };

  return (
    <>
      <button onClick={onClick}>Click me</button>
    </>
  );
};

export default Component2;

provider/ContextProvider.jsx:

import { createContext, useContext, useState } from "react";

const Context = createContext();

const ContextProvider = ({ children }) => {
  const [token, setToken] = useState("Initial");

  return (
    <Context.Provider value={{ token, setToken }}>{children}</Context.Provider>
  );
};

export const provideContext = () => {
  return useContext(Context);
};

export default ContextProvider;

When clicking the button in Component2, the URL changes but the UI still shows Component2. Component 1 is not even rendered once.

Interestingly, if i remove the setTimeout() in Component2.jsx function OR remove const { token } = provideContext(); in Routes.jsx, the issue goes away. Not sure what is going.


Solution

  • Issue is declaring the router inside the ReactTree. The setToken("Token"); enqueues a state update in ContextProvider and triggers consumers like Routes to rerender, and when Routes renders it redeclares a new router which interrupts any current navigation actions being processed.

    Either move declaring the router outside the ReactTree:

    import { RouterProvider, createBrowserRouter } from "react-router-dom";
    import { provideContext } from "./provider/ContextProvider";
    import Component1 from "./pages/Component1";
    import Component2 from "./pages/Component2";
    
    const router = createBrowserRouter([
      {
        path: "/Component2",
        element: <Component2 />,
      },
      {
        path: "/Component1",
        element: <Component1 />,
      },
    ]);
    
    const Routes = () => {
      const { token } = provideContext();
    
      return <RouterProvider router={router} />;
    };
    
    export default Routes;
    

    Or memoize the router so it's not redeclared each render cycle when the context state updates:

    import { useMemo } from "react";
    import { RouterProvider, createBrowserRouter } from "react-router-dom";
    import { provideContext } from "./provider/ContextProvider";
    import Component1 from "./pages/Component1";
    import Component2 from "./pages/Component2";
    
    const Routes = () => {
      const { token } = provideContext();
    
      const router = useMemo(() =>
        createBrowserRouter([
          {
            path: "/Component2",
            element: <Component2 />,
          },
          {
            path: "/Component1",
            element: <Component1 />,
          },
        ]), [] // <-- add any required dependencies here
      );
    
      return <RouterProvider router={router} />;
    };
    
    export default Routes;
    

    You really shouldn't need to use a setTimeout to issue the navigation action. Remove setTimeout in Component2.

    import { useNavigate } from "react-router-dom";
    import { provideContext } from "../provider/ContextProvider";
    
    const Component2 = () => {
      const navigate = useNavigate();
      const { setToken } = provideContext();
    
      const onClick = () => {
        setToken("Token");
        navigate("/Component1");
      };
    
      return (
        <>
          <button onClick={onClick}>Click me</button>
        </>
      );
    };
    
    export default Component2;