Search code examples
reactjstypescriptreact-router-dom

How should the createBrowserRouter and RouterProvider be use with application context


Any big application will have general context that might need the router to redirect user. Since the RouterProvider can only be use at the root level. How do you use it with application context.

For example I have an ApiContext that redirect to a 403 route/component if the user does not have the right to access the API.

How to I wrap this context in the RouterProvider without having to rerender it on each route change. In other word. How can I have parent context that use the router?

It's working fine if I use the BrowserRouter, but the only way to use any kind of prompt before unload in v6.8 is to use the data api and therefore the createBrowserRouter method.

The same goes with header navigation. I don't want my header to be in every route component and part of the routing. I want my header to be static and route component to be dynamic.

// Aplication Context
export const ApiContext = React.createContext({});

export const ApiProvider = memo((p: { children: React.ReactNode; }) => {
  const navigate = useNavigate();
  const [token, setToken] = useState<string>()

  if (!token) {
    navigate('/403');
  }

  const value = useMemo(() => ({}), []);
  return (
    <ApiContext.Provider value={value}>
      {p.children}
    </ApiContext.Provider>
  );
});
// Index
const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    children: [
      {
        path: "403",
        element: <Login />,
      },
    ],
  },
]);

ReactDOM.createRoot(document.getElementById("root")).render(
  <ApiProvider>
    <Header />
    <RouterProvider router={router} />
  </ApiProvider>
);

Solution

  • It's not actually factual that the RouterProvider be the root node/element/component of the app, it just needs to be higher in the ReactTree than any rendered component needing to access the routing context it provides.

    If you are rendering components that need access to the routing context provided by a router, e.g. useNavigate, etc, then they necessarily need to be rendered under the ReactTree created by the router provider, i.e. the router component.

    Move the ApiProvider into the router on a layout route. The ApiProvider component should now also render an Outlet for nested routes to render their element content into instead of a children prop.

    The ApiProvider component should also not call navigate as an unintentional side-effect directly in the component body. Either move the if (!token) check and redirect into a useEffect hook or just render a redirect with the Navigate component.

    Example:

    Application Context

    import { Navigate, Outlet } from 'react-router-dom';
    
    export const ApiContext = React.createContext({});
    
    export const ApiProvider = () => {
      const [token, setToken] = useState<string>("");
    
      const value = useMemo(() => ({}), []);
    
      if (!token) {
        return <Navigate to="/403" replace />;
      }
    
      return (
        <ApiContext.Provider value={value}>
          <Header />
          <Outlet />
        </ApiContext.Provider>
      );
    };
    

    Index

    const router = createBrowserRouter([
      {
        element: <ApiProvider />,
        children: [
          {
            path: "/",
            element: <Root />,
            loader: rootLoader,
            children: [
              {
                path: "team",
                element: <Team />,
                loader: teamLoader,
              },
            ],
          },
        ],
      },
    ]);
    
    ReactDOM.createRoot(document.getElementById("root")).render(
      <RouterProvider router={router} />
    );