I’m messing around with a Shell application in React and wanting to load information from apps/components that are added to the shell dynamically, i.e. via auto-discovery.
Now, we’re using React for this example so we can create React apps using Vite for what we will call, our components, as these can be loaded into React as components. The reason to create these as standalone apps would be for use to develop against.
So let’s assume I have a main application, the Shell app and this can get routes for functionality (our apps/components) which may get added later on in the development process, basically a pluggable type of architecture which just uses React components along with those components supplying routing information to allow us to plug them into the Shell.
To give it more context, I build a Shell application with Search and later on want to add a Document UI, so it’d be nice if the Document can be worked on separately and when ready, deployed to a specific locations where the Shell (upon refresh) can discover the new component and wire into the Shell.
Vite has a feature import.meta.glob which we can use to discover our routes.tsx that are deployed to a specific path, i.e.
const modules = import.meta.glob<Record<string, unknown>>(
'../../apps/*/src/routes.tsx', { eager: true }
);
This will return keys for the file paths and values being the functions that import from the files or modules. For example it might locate components such as
apps/search/src/routes.tsx apps/document/src/routes.tsx apps/export/src/routes.tsx apps/settings/src/routes.tsx
If eager is missing (or false) then Vite lazy imports the functions and you’d then need to call them yourself, with eager:true, Vite imports the modules immediately.
If we use something like this
import type { RouteObject } from 'react-router-dom';
const modules = import.meta.glob<Record<string, unknown>>(
'../../apps/*/src/routes.tsx', { eager: true }
);
export const allAppRoutes: RouteObject[] = Object.values(modules)
.flatMap((mod) => {
const arr = Object.values(mod).find(
(v): v is RouteObject[] => Array.isArray(v) &&
v.every(item => typeof item === 'object' &&
item !== null && 'path' in item && 'element' in item)
);
return arr || [];
});
Then we could use the routes via the react-router-dom in App.tsx, like this
const baseRoutes = [
{ path: '/', element: <div>Welcome to the Shell</div> },
];
const routes = [
...baseRoutes,
...allAppRoutes,
];
return (
<AuthContext.Provider value={auth}>
<ShellLayout>
<Suspense fallback={<div>Loading…</div>}>
<Routes>
{routes.map(({ path, element }) => (
<Route key={path} path={path} element={element} />
))}
</Routes>
</Suspense>
</ShellLayout>
</AuthContext.Provider>
);
and finally here’s an example routes.tsx file for our components
import SearchApp from './App';
export const searchRoutes = [
{ path: '/search/*', element: <SearchApp /> },
];