I'm using React-Router with dynamic route modules in my React app. The loader function for one of my routes doesn't seem to execute, and I don't see the expected console.log
in the browser console.
Here's the relevant setup:
App.jsx
import React, { lazy } from "react";
import {
RouterProvider,
createBrowserRouter,
createRoutesFromElements,
Route,
} from "react-router-dom";
import ProtectedRoute from "./app/layout/ProtectedRoute.jsx";
// Lazy-loaded components
const ErrorPage = lazy(() => import("./layout/ErrorPage"));
import WorkplaceLayout from "./layout/WorkplaceLayout";
const LoginPage = lazy(() => import("./auth/LoginPage"));
// Dynamic module loader
const modules = {
retail: lazy(() => import("./modules/retail/MainRoutes")),
inventory: lazy(() => import("./modules/inventory/MainRoutes")),
};
const DynamicModuleRoutes = () => {
const defaultModule = 'retail'; // or from localStorage
return (
<React.Suspense fallback={<div>Loading {defaultModule} module...</div>}>
<ModuleRoutes />
</React.Suspense>
);
};
// Define the router with loaders and error elements
const router = createBrowserRouter(
createRoutesFromElements(
<>
<Route element={<ProtectedRoute />}>
<Route path="workplace" element={<WorkplaceLayout />}>
<Route
path=":defaultModule/*"
element={<DynamicModuleRoutes />}
/>
{/* other routes */}
</Route>
</Route>
<Route
path="login"
element={<LoginPage />}
errorElement={<ErrorPage />}
/>
<Route path="*" element={<div>Unknown page</div>} />
</>
)
);
const App = () => {
return <RouterProvider router={router} />;
};
export default App;
WorkplaceLayout.jsx
import React, { Suspense } from "react";
import { Outlet } from "react-router-dom";
const WorkplaceLayout = () => {
return (
<div className="workplace">
<div className="app-sidebar">
<div>Logo</div>
<div>Menus</div>
</div>
<div className="canvas">
{/* here I want to display loading */}
<Suspense fallback={<h3>Loading</h3>}>
<Outlet />
</Suspense>
</div>
</div>
);
};
export default WorkplaceLayout;
Retail MainRoutes.jsx
import React from "react";
import { Route, Routes } from "react-router-dom";
import DashboardPage, { loader as dashboardLoader } from "./pages/Dashboard";
const MainRoutes = () => (
<Routes>
<Route
index
element={<DashboardPage />}
loader={dashboardLoader}
errorElement={<div>Error loading Dashboard</div>}
/>
<Route path="invoice" element={<div>Invoices Page</div>} />
<Route path="*" element={<div>Unknown retail page</div>} />
</Routes>
);
export default MainRoutes;
Dashboard.jsx
import React from "react";
import { useLoaderData } from "react-router-dom";
const Dashboard = () => {
const data = useLoaderData();
return <div>Dashboard Data: {data}</div>;
};
export function loader() {
console.log("Loader executed"); // this never appears
return new Promise((resolve) => {
setTimeout(() => resolve("Dashboard data loaded"), 3000);
});
}
export default Dashboard;
The DynamicModuleRoutes
component dynamically loads route modules. Everything renders correctly, but the loader function isn't being triggered. The console.log("Loader executed")
never appears.
Any ideas on why the loader isn't executing or how to debug this further?
The issue here is that the code isn't rendering the "dynamic" routes/components as nested routes, but instead they are rendered as descendent routes.
Nested Routes:
Route
directly wraps child Route
Route
component renders an Outlet
for the nested children Route
components' element
to be rendered into"/root/segment/*"
DescendentRoutes:
Route
components renders another Routes
and set of descendent Route
components"/root/segment/*"
DynamicModuleRoutes
is a component rendering descendent routes.
<Route
path=":defaultModule/*" // <-- trailing splat matcher
element={<DynamicModuleRoutes />}
/>
const DynamicModuleRoutes = () => {
const defaultModule = 'retail'; // or from localStorage
return (
<React.Suspense fallback={<div>Loading {defaultModule} module...</div>}>
<ModuleRoutes />
</React.Suspense>
);
};
const MainRoutes = () => (
<Routes> {/* <-- descendent Routes wrapper */}
{/* descendent Routes */}
<Route
index
element={<DashboardPage />}
loader={dashboardLoader}
errorElement={<div>Error loading Dashboard</div>}
/>
<Route path="invoice" element={<div>Invoices Page</div>} />
<Route path="*" element={<div>Unknown retail page</div>} />
</Routes>
);
Route loaders only work in Data Routers, sure, but the caveat is that the routes must be known at declaration time. They can not be dynamically "loaded" in later at runtime.
I suspect you could refactor the code a bit to convert the descendent routes into actual nested routes so any lazily loaded components' loaders/actions/etc can work with the Data router.
Update DynamicModuleRoutes
to render an Outlet
instead of the child route component.
const DynamicModuleRoutes = () => {
const defaultModule = 'retail'; // or from localStorage
return (
<React.Suspense fallback={<div>Loading {defaultModule} module...</div>}>
<Outlet />
</React.Suspense>
);
};
const router = createBrowserRouter(
createRoutesFromElements(
<>
<Route element={<ProtectedRoute />}>
<Route path="workplace" element={<WorkplaceLayout />}>
<Route
path=":defaultModule"
element={<DynamicModuleRoutes />}
>
{/* Lazily loaded route(s) */}
<Route index lazy={() => import("./pages/Dashboard")} />
{/* Regularly loaded route(s) */}
<Route path="invoice" element={<div>Invoices Page</div>} />
<Route path="*" element={<div>Unknown retail page</div>} />
</Route>
{/* other routes */}
</Route>
</Route>
<Route
path="login"
element={<LoginPage />}
errorElement={<ErrorPage />}
/>
<Route path="*" element={<div>Unknown page</div>} />
</>
)
);
Update the Dashboard file to export specifically named Component
, loader
, ErrorBoundary
/errorElement
, and anything else that the Route
consumes as a prop (e.g. action
, etc).
Dashboard.jsx
import { useLoaderData } from "react-router-dom";
export function Component() {
const data = useLoaderData();
return <div>Dashboard Data: {data}</div>;
};
export function ErrorBoundary() {
return <div>Error loading Dashboard</div>;
}
export function loader() {
console.log("Loader executed");
return new Promise((resolve) => {
setTimeout(() => resolve("Dashboard data loaded"), 3000);
});
}