Kairei
Kairei

Reputation: 59

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.

Upvotes: 1

Views: 1435

Answers (2)

Drew Reese
Drew Reese

Reputation: 203373

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>
);

Upvotes: 0

Kairei
Kairei

Reputation: 59

I have a working solution. I'm posting what I'm doing in case it might help someone else but I'm guessing there might be a better solution so hopefully we'll get a better answer at some point.

It is really simple actually but being new to react and react router, it took me a bit... so I'm loading a wildcard route initially that does nothing but load an "App Initializer" component and I pass the "root" object to it e.g.:

const container = document.getElementById('root')!;
const root = createRoot(container);
const router = createBrowserRouter([
    {
        path: "*",
        element: <AppInitializer root={root} />
    }
]);

In that AppInitializer I show a progress spinner and use RTK query to load my routes from the database. Then I just props.root.render(...the dynamically created React.StrictMode/ThemeContextProvider/Provider/RouteProvider component tree...) and I'm done.

Maybe this is fine but I'm not particularly happy with this solution. For example, I feel like I'm creating the entire redux store to do one query only to wipe it out and re-create it when I re-render the root. It is nothing I can notice performance-wise, but it seems inelegant (I'd rather just update the routes rather than re-render the entire app's component tree).

Upvotes: 0

Related Questions