The MicroApp system is how @esmx/router manages framework-agnostic micro-frontends. Each micro-app provides three lifecycle methods: mount, unmount, and optionally renderToString. The router handles transitions between micro-apps during navigation.
The interface every micro-app must implement.
interface RouterMicroAppOptions {
mount: (el: HTMLElement) => void;
unmount: () => void;
renderToString?: () => Awaitable<string>;
}(el: HTMLElement) => voidMount the application into the given DOM element. Called when the router navigates to a route bound to this micro-app.
el: HTMLElement - The DOM element to mount into (from RouterOptions.root)() => voidClean up and destroy the application. Called when navigating away to a route bound to a different micro-app.
() => Awaitable<string>Return the SSR HTML string for the current state of the application. Called by router.renderToString() during server-side rendering.
A factory function that creates a micro-app, receiving the router instance.
type RouterMicroAppCallback = (router: Router) => RouterMicroAppOptions;The apps option in RouterOptions accepts either a map of named factories or a single factory.
type RouterMicroApp =
| Record<string, RouterMicroAppCallback | undefined>
| RouterMicroAppCallback;Micro-apps are registered via the apps option on the Router and referenced by the app property in route configs:
const router = new Router({
root: '#app',
routes: [
{
path: '/react',
app: 'react',
children: [
{ path: '', component: ReactHome },
{ path: 'about', component: ReactAbout }
]
},
{
path: '/vue',
app: 'vue',
children: [
{ path: '', component: VueHome }
]
}
],
apps: {
react: (router) => createReactApp(router),
vue: (router) => createVueApp(router)
}
});import * as ReactDOM from 'react-dom/client';
import * as ReactDOMServer from 'react-dom/server';
function createReactApp(router: Router): RouterMicroAppOptions {
let root: ReactDOM.Root | null = null;
return {
mount(el: HTMLElement) {
root = ReactDOM.createRoot(el);
root.render(<App router={router} />);
},
unmount() {
root?.unmount();
root = null;
},
async renderToString() {
return ReactDOMServer.renderToString(<App router={router} />);
}
};
}import { createApp, createSSRApp } from 'vue';
import { renderToString as vueRenderToString } from 'vue/server-renderer';
function createVueApp(router: Router): RouterMicroAppOptions {
let app: VueApp | null = null;
return {
mount(el: HTMLElement) {
app = createApp(App);
app.provide('router', router);
app.mount(el);
},
unmount() {
app?.unmount();
app = null;
},
async renderToString() {
const ssrApp = createSSRApp(App);
ssrApp.provide('router', router);
return await vueRenderToString(ssrApp);
}
};
}When a route is matched, the router determines which micro-app to use:
app property is usedapp is a string, it's looked up in router.options.appsapp is a function, it's called directly as the factoryWhen navigating between routes with different app values:
1. New app factory is called → creates new RouterMicroAppOptions
2. new app.mount(rootElement) → mount into DOM
3. old app.unmount() → clean up previous appWhen navigating within the same app (e.g., /react → /react/about):
router.restartApp() forces a full unmount → mount cycle even if the app key hasn't changed.
During server-side rendering:
// 1. Create router with request context
const router = new Router({
base: new URL(`http://localhost${req.url}`),
mode: RouterMode.memory,
req,
res,
routes,
apps
});
// 2. Navigate to the requested URL
await router.push(req.url);
// 3. Render the micro-app to HTML
const html = await router.renderToString();The root option in RouterOptions determines where micro-apps are mounted:
<div> is created and appended to document.body