MicroApp

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.

RouterMicroAppOptions

The interface every micro-app must implement.

  • Type Definition:
interface RouterMicroAppOptions {
  mount: (el: HTMLElement) => void;
  hydration?: (el: HTMLElement) => void;
  unmount: () => void;
  renderToString?: () => Awaitable<string>;
}

mount

  • Type: (el: HTMLElement) => void

Mount the application into the given DOM element. Called when the router navigates to a route bound to this micro-app.

hydration

  • Type: (el: HTMLElement) => void

Hydrate server-rendered markup instead of mounting from scratch. When the container produced by router.renderToString() carries the data-ssr marker, the router calls hydration with the existing SSR root element rather than mount. If SSR content is present but no hydration function is provided, the router throws an error.

  • Parameters:
    • el: HTMLElement - The pre-rendered SSR root element to hydrate into

unmount

  • Type: () => void

Clean up and destroy the application. Called when navigating away to a route bound to a different micro-app.

renderToString

  • Type: () => Awaitable<string>

Return the SSR HTML string for the current state of the application. Called by router.renderToString() during SSR.

RouterMicroAppCallback

A factory function that creates a micro-app, receiving the router instance.

  • Type Definition:
type RouterMicroAppCallback = (router: Router) => RouterMicroAppOptions;

RouterMicroApp

The apps option in RouterOptions accepts either a map of named factories or a single factory.

  • Type Definition:
type RouterMicroApp =
  | Record<string, RouterMicroAppCallback | undefined>
  | RouterMicroAppCallback;

Usage

Registering Micro-Apps

Micro-apps are registered via the apps option on the Router and referenced by the app property in route configs:

const router = new Router({
  appId: '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)
  }
});

React Example

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} />);
    }
  };
}

Vue 3 Example

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);
    }
  };
}

Lifecycle

App Selection

When a route is matched, the router determines which micro-app to use:

  1. The first matched route config with an app property is used
  2. If app is a string, it's looked up in router.options.apps
  3. If app is a function, it's called directly as the factory

App Transition

When 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 app

When navigating within the same app (e.g., /react/react/about):

  • No mount/unmount occurs
  • The app handles internal routing via its own component system

Force Restart

router.restartApp() forces a full unmount → mount cycle even if the app key hasn't changed.

SSR Flow

During SSR:

// 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();

Root Element

The appId option in RouterOptions determines where micro-apps are mounted:

  • If the element exists in the DOM, it's reused
  • If it doesn't exist, a <div> is created and appended to document.body
  • Layer routers create their own root elements with overlay styling