Loading React Component by Convention
Next.js uses file-system based routing, meaning you can use folders and files to define routes. Basically, you don’t need to define routes, and if you access the app by a folder and file path, you can get the component. React Router can also do this. Instead of doing the following:
<Route path="/demo/Button" element={<Button />} />
<Route path="/demo/Card" element={<Card />} />
//...
We can load the component by path:
<Route path="/demo/:componentName" element={<ComponentAtPath />} />
By following a convention in routing, we can save a lot of configuration. So,
convention over configuration for the win. Here is how the ComponentAtPath
is
implemented:
import { lazy, Suspense } from "react";
import { Navigate, useParams } from "react-router-dom";
export const ComponentAtPath = () => {
const { componentName } = useParams();
const path = `./demo/${componentName}`;
const Component = lazy(() =>
import(path).catch(() => ({ default: () => <Navigate to="/404" /> }))
);
return (
<Suspense fallback={<div>Loading...</div>}>
<Component />
</Suspense>
);
};
If I have more components, I don’t need to add more route entries. This is easy for me to showcase my React prototypes. So I use the following to create a list of my components:
export const ComponentList = () => {
const components = [
"Button, A simple button component",
"Card, A simple card component",
"Dialog, Dialog which does not exist",
];
return (
<div>
<h2>Select a Component</h2>
<ul>
{components.map((item) => {
const [componentName, description] = item.split(",");
return (
<li key={componentName}>
<Link to={`/demo/${componentName}`}>
{description || componentName}
</Link>
</li>
);
})}
</ul>
</div>
);
};
export function App() {
return (
<BrowserRouter>
<Routes>
<Route path="./demo/:componentName" element={<ComponentAtPath />} />
<Route path="404" element={<h1>Page Not Found</h1>} />
<Route path="" element={<ComponentList />} />
</Routes>
</BrowserRouter>
);
}
This works in development time, but when you deploy to production, it no longer
works. In production, tools like Vite analyze and bundle your code ahead of
time. When you use a dynamic string for import(path)
, the bundler can’t
statically analyze and pre-bundle those components because the path is dynamic.
This means that the component is not included in the build output, causing it to
fail when trying to load it at runtime. To solve this problem, we need to ensure
that the dynamic import path is resolvable at build time. One way to solve this
is to use static imports for known components and use dynamic imports only for
specific cases where the component name is known upfront. We can also try to
organize our components in a way that allows the bundler to optimize them
correctly. Here is the revised code:
// Define a static glob pattern to match all components
const componentFiles = import.meta.glob(`./demo/*.tsx`);
export const ComponentAtPath = () => {
const { componentName } = useParams();
// Dynamically select the component based on componentName
const componentPath = `./demo/${componentName}.tsx`;
// Check if the component is available in the glob mapping
const Component = lazy(() => {
if (componentFiles[componentPath]) {
return componentFiles[componentPath]().then((module) => ({
default: module.default,
}));
} else {
return Promise.resolve({ default: () => <Navigate to="/404" /> });
}
});
return (
<Suspense fallback={<div>Loading...</div>}>
<Component />
</Suspense>
);
};
The full source code can be found here. The demo of the app is as follows: