Search code examples
react-reduxreact-router-dom

Set React RouterProvider "router" Routes from API


I'm using react-router-dom version 6.14.1 in a react/redux project. I got it working where I can specify routes in index.tsx by programatically creating a router:

import {
  createBrowserRouter,
  RouterProvider
} from "react-router-dom";

const router = createBrowserRouter([ /*...routes specified in code here... */ ]);

...and then rendering a RouterProvider component to which I pass the above router.

root.render(
  <React.StrictMode>
    <ThemeContextProvider>
      <Provider store={store}>
        <RouterProvider router={router} />
      </Provider>
    </ThemeContextProvider>
  </React.StrictMode>
);

I am hard-coding the routes passed to createBrowserRouter right now but what I really want to do is load them from my database via an API call. Unfortunately, I am using Redux's RTK query and I can't use that in index.tsx since it doesn't all seem to get set up until the "Provider" component gets loaded. So, I seem to have a chicken and egg situation where I can't get the routes from data until I load some components but I can't load the components until I do my root.render which needs the RouteProvider component set up with routes.

Is there a proper architecture/approach for this type of situation?

I've spent hours trying all kinds of weird things (like trying to load a dummy component that loads the route data using RTK query and then calling back to the parent index.tsx code (via a function I pass to the dummy component) to update the router variable or to just somehow trying to get a reference to that "router" object from the dummy component and pushing new items into its "routes" array) but nothing works. Feels like I'm just doing this all wrong. I just want to be able to, at any time, update the routes in that RouterProvider.


Solution

  • My suggestion would be to create a component that conditionally renders the RouterProvider once the routing data has been fetched. It's similar to your answer but doesn't throw one ReactTree away for another.

    Something like the following:

    const App = () => {
      const dispatch = useDispatch();
      const routes = useSelector(.....);
    
      useEffect(() => {
        dispatch(fetchRoutes());
      }, [dispatch]);
    
      const router = useMemo(() => {
        if (routes) {
          return createBrowserRouter(routes);
        }
        return null;
      }, [routes]);
    
      return router
        ? <RouterProvider router={router} />
        : (
          ... loading UI/UX ...
        );
    };
    
    root.render(
      <React.StrictMode>
        <ThemeContextProvider>
          <Provider store={store}>
            <App />
          </Provider>
        </ThemeContextProvider>
      </React.StrictMode>
    );