Reputation: 59
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
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
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