Runtime API

MF runtime APIs are built around the ModuleFederation instance. The default instance is usually created automatically by the build plugin, so you do not need to pass an instance explicitly when calling runtime APIs — they automatically apply to the default instance in the runtime.

createInstance

Used to create a new ModuleFederation instance. Unlike other runtime APIs, createInstance does not act on the default instance; it returns a brand-new instance that is isolated from the default one.

When to use

The default instance pattern already covers most scenarios. You only need createInstance in the following cases:

  • No build plugin is used (pure runtime scenario)
  • You need to create multiple ModuleFederation instances with different configurations in the same application
  • You want to leverage Module Federation's partitioning feature to encapsulate a set of APIs for use by other projects

Behavior

  • Every call creates a brand-new instance and adds it to the global instance list
  • It does not look up existing instances by name / version, nor does it merge the new options into an existing instance
  • The new instance does not replace the default instance; if you need to retrieve it elsewhere, use getInstance

Example

import { createInstance } from '@module-federation/enhanced/runtime';

const mf = createInstance({
  name: 'host',
  remotes: [
    {
      name: 'sub1',
      entry: 'http://localhost:8080/mf-manifest.json'
    }
  ]
});

mf.loadRemote('sub1/util').then((m) => m.add(1, 2, 3));

init Use with caution

Used to initialize or reuse a ModuleFederation runtime instance.

init does not always create a new instance: when called, the runtime first looks for an existing instance by name and version. If a matching one is found, it is reused and the new options are merged via instance.initOptions(options). Only when no match is found will a new instance be created.

Therefore, init is better suited for scenarios where the same host is initialized multiple times and you want to reuse and extend the same runtime instance.

Warning
  • If you need to create a completely independent new instance, use createInstance.
  • If you only want to access the instance created by the build plugin, use getInstance.
  • Type: init(options: InitOptions): ModuleFederation
  InitOptions Type
type InitOptions {
  // Name of the current host
  name: string;
  // List of dependent remote modules
  // tip: The remotes configured at runtime are not completely consistent in type and data with those passed in by the build plugin.
  remotes: Array<RemoteInfo>;
  // List of dependencies that the current host needs to share
  // When using the build plugin, users can configure the dependencies to be shared in the build plugin, and the build plugin will inject the dependencies to be shared into the shared configuration at runtime.
  // When shared is passed in at runtime, the version instance reference must be passed in manually, because it cannot be directly obtained at runtime.
  shared?: {
    [pkgName: string]: ShareArgs | ShareArgs[];
  };
};

type ShareArgs =
  | (SharedBaseArgs & { get: SharedGetter })
  | (SharedBaseArgs & { lib: () => Module });

type SharedBaseArgs = {
  version: string;
  shareConfig?: SharedConfig;
  scope?: string | Array<string>;
  deps?: Array<string>;
  strategy?: 'version-first' | 'loaded-first';
};

type SharedGetter = (() => () => Module) | (() => Promise<() => Module>);

type RemoteInfo = {
  alias?: string;
};

interface RemotesWithEntry {
  name: string;
  entry: string;
}

type ShareInfos = {
  // Package name and basic information of the dependency, sharing strategy
  [pkgName: string]: Share;
};

type Share = {
  // Version of the shared dependency
  version: string;
  // Which modules consume the current shared dependency
  useIn?: Array<string>;
  // Which module the shared dependency comes from
  from?: string;
  // Factory function to get the instance of the shared dependency. When the cached shared instance cannot be loaded, it will load its own shared dependency.
  lib: () => Module;
  // Sharing strategy, which strategy will be used to determine the reuse of shared dependencies
  shareConfig?: SharedConfig;
  // Dependencies between shares
  deps?: Array<string>;
  // Under which scope the current shared dependency is placed, the default is default
  scope?: string | Array<string>;
};
  Example
import { init, loadRemote } from '@module-federation/enhanced/runtime';

init({
  name: "mf_host",
  remotes: [
    {
      name: "remote",
      // After configuring an alias, it can be loaded directly through the alias
      alias: "app1",
      // Decide which module to load by specifying the address of the module's manifest.json file
      entry: "http://localhost:2001/mf-manifest.json"
    }
  ],
});
Recommended migration

If your code currently uses init, you can migrate to the recommended approach based on your scenario.

Migration option - using the build plugin

Remove the init call, use getInstance to retrieve the instance, and use registerRemotes / registerShared / registerPlugins to register the options that were previously passed to init.

- import { init } from '@module-federation/enhanced/runtime';
+ import { registerShared, registerRemotes, registerPlugins, getInstance } from '@module-federation/enhanced/runtime';
  import React from 'react';
  import mfRuntimePlugin from 'mf-runtime-plugin';

- const instance = init({
-   name: 'mf_host',
-   remotes: [
-     {
-       name: 'remote',
-       entry: 'http://localhost:2001/mf-manifest.json',
-     },
-   ],
-   shared: {
-     react: {
-       version: '18.0.0',
-       scope: 'default',
-       lib: () => React,
-       shareConfig: {
-         singleton: true,
-         requiredVersion: '^18.0.0',
-       },
-     },
-   },
-   plugins: [mfRuntimePlugin()],
- });
+ const instance = getInstance();
+ registerRemotes([
+   {
+     name: 'remote',
+     entry: 'http://localhost:2001/mf-manifest.json',
+   },
+ ]);
+ registerShared({
+   react: {
+     version: '18.0.0',
+     scope: 'default',
+     lib: () => React,
+     shareConfig: {
+       singleton: true,
+       requiredVersion: '^18.0.0',
+     },
+   },
+ });
+ registerPlugins([mfRuntimePlugin()]);

Migration option - pure runtime

Switch directly to createInstance; the option shape stays the same. But note the different semantics: createInstance always creates a new instance, while init may reuse an existing instance with the same name / version and merge the new options into it.

- import { init } from '@module-federation/enhanced/runtime';
+ import { createInstance } from '@module-federation/enhanced/runtime';

- const instance = init({
+ const instance = createInstance({
    name: 'mf_host',
    remotes: [
      {
        name: 'remote',
        entry: 'http://localhost:2001/mf-manifest.json',
      },
    ],
    shared: {
      react: {
        version: '18.0.0',
        scope: 'default',
        lib: () => React,
        shareConfig: {
          singleton: true,
          requiredVersion: '^18.0.0',
        },
      },
    },
    plugins: [mfRuntimePlugin()],
  });

getInstance

  • Type: getInstance(): ModuleFederation | null
  • Type: getInstance(finder: (instance: ModuleFederation) => boolean): ModuleFederation | null
  • Retrieves the default ModuleFederation instance, or the first registered instance that matches a finder callback

Once the default instance has been created by the build plugin or by init, you can call getInstance() to retrieve it.

import { getInstance } from '@module-federation/enhanced/runtime';

const mfInstance = getInstance();
if (!mfInstance) {
  throw new Error('Module Federation instance is not initialized');
}

mfInstance.loadRemote('remote/util');

If the build plugin is not used, calling getInstance will throw an exception. In that case you need to use createInstance to create a new instance.

Instances created via createInstance do not replace the default instance, but they are still registered in the global instance list. So even if you did not keep the returned reference, you can still find them later by passing a finder callback to getInstance.

The finder callback behaves like Array.prototype.find: the runtime iterates over the currently registered instances and returns the first match. If no matching instance is found, getInstance returns null.

const targetInstance = getInstance(
  (instance) => instance.name === 'remote-host',
);

if (targetInstance) {
  targetInstance.loadRemote('remote/util');
}

registerRemotes

  Type declaration
function registerRemotes(remotes: Remote[], options?: { force?: boolean }) {}

type Remote = (RemoteWithEntry | RemoteWithVersion) & RemoteInfoCommon;

interface RemoteInfoCommon {
  alias?: string;
  shareScope?: string;
  type?: RemoteEntryType;
  entryGlobalName?: string;
}

interface RemoteWithEntry {
  name: string;
  entry: string;
}

interface RemoteWithVersion {
  name: string;
  version: string;
}
Warning

When force: true is set, the newly registered modules will overwrite already-registered and loaded modules, and the cache of the loaded modules will be automatically deleted. A warning will also be printed to the console to inform you that this operation is risky.

Build Plugin (Use build plugin)
Pure Runtime (Not use build plugin)
import { registerRemotes } from '@module-federation/enhanced/runtime';

// register new remote sub2
registerRemotes([
  {
    name: 'sub2',
    entry: 'http://localhost:2002/mf-manifest.json',
  }
]);

// override remote sub1
registerRemotes([
  {
    name: 'sub1',
    entry: 'http://localhost:2003/mf-manifest.json',
  }
], { force: true });

registerPlugins

function registerPlugins(plugins: ModuleFederationRuntimePlugin[]) {}
Build Plugin (Use build plugin)
Pure Runtime (Not use build plugin)
import { registerPlugins } from '@module-federation/enhanced/runtime';
import runtimePlugin from './custom-runtime-plugin';

// add new runtime plugin
registerPlugins([runtimePlugin()]);

registerPlugins([
  {
    name: 'custom-plugin-runtime',
    beforeInit(args) {
      const { userOptions, origin } = args;
      if (origin.options.name && origin.options.name !== userOptions.name) {
        userOptions.name = origin.options.name;
      }
      console.log('[build time inject] beforeInit: ', args);
      return args;
    },
    beforeLoadShare(args) {
      console.log('[build time inject] beforeLoadShare: ', args);
      return args;
    },
    createLink({ url }) {
      const link = document.createElement('link');
      link.setAttribute('href', url);
      link.setAttribute('rel', 'preload');
      link.setAttribute('as', 'script');
      link.setAttribute('crossorigin', 'anonymous');
      return link;
    },
  }
]);

registerGlobalPlugins

function registerGlobalPlugins(plugins: ModuleFederationRuntimePlugin[]): void {}

Registers plugins on the global federation state instead of on a single current instance. This is suitable for scenarios such as:

  • shared instrumentation
  • environment-wide policy
  • host-wide default plugins

Global plugins are deduplicated by plugin.name.

import { registerGlobalPlugins, createInstance } from '@module-federation/enhanced/runtime';

import runtimePlugin from './runtime-plugin';

registerGlobalPlugins([runtimePlugin()]);

const mf = createInstance({
  name: 'mf_host',
  remotes: [
    {
      name: 'sub1',
      entry: 'http://localhost:2001/mf-manifest.json',
    },
  ],
});

For predictable behavior, register global plugins before creating or using runtime instances.

registerShared

Registers shared dependencies for the host. The runtime will prefer to reuse a shared dependency that already exists globally and satisfies the conditions; otherwise it falls back to the dependency registered here.

function registerShared(shared: Shared): void;

type Shared = {
  // Mapping of shared dependency package names; the same package can be registered with multiple versions (array form)
  [pkgName: string]: ShareArgs | ShareArgs[];
};

/**
 * ShareArgs must provide either lib or get
 *
 * lib: synchronous factory function that returns the module immediately when called; should not return a Promise.
 * Suitable when the module is already available at the time `registerShared` is called, e.g. `import React from 'react'`
 *
 * get: used for async / lazy loading scenarios
 * */
type ShareArgs =
  | (SharedBaseArgs & { lib: () => Module })
  | (SharedBaseArgs & { get: SharedGetter })
  | SharedBaseArgs;

type SharedBaseArgs = {
  // Version number being registered; recommended for version differentiation
  version?: string;
  // Sharing strategy, controlling singleton, requiredVersion, etc.
  shareConfig?: SharedConfig;
  // Scope it belongs to, defaults to 'default'; can be placed under multiple scopes simultaneously
  scope?: string | Array<string>;
  // Names of other shared dependencies that this one depends on
  deps?: Array<string>;
  // Version selection strategy: prefer by version number / prefer already loaded instance
  strategy?: 'version-first' | 'loaded-first';
  loaded?: boolean;
};

interface SharedConfig {
  singleton?: boolean;
  requiredVersion: false | string;
  eager?: boolean;
  strictVersion?: boolean;
  layer?: string | null;
}

type SharedGetter = (() => () => Module) | (() => Promise<() => Module>);
Build Plugin (Use build plugin)
Pure Runtime (Not use build plugin)
import { registerShared } from '@module-federation/enhanced/runtime';
import React from 'react';
import ReactDom from 'react-dom';

registerShared({
  react: {
    version: '18.0.0',
    scope: 'default',
    lib: () => React,
    shareConfig: {
      singleton: true,
      requiredVersion: '^18.0.0',
    },
  },
  'react-dom': {
    version: '18.0.0',
    scope: 'default',
    lib: () => ReactDom,
    shareConfig: {
      singleton: true,
      requiredVersion: '^18.0.0',
    },
  },
  antd: {
    version: '1.0.0',
    scope: 'default',
    get: () => import('antd').then((m) => () => m),
  },
});

loadShare

type loadShare = (
  pkgName: string,
  extraOptions?: {
    customShareInfo?: Partial<Shared>;
    resolver?: (sharedOptions: ShareInfos[string]) => Shared;
  }
) => Promise<() => ShareModule>;

Gets a share dependency. When there is a share dependency in the global environment that meets the requirements of the current host, the existing dependency that meets the share conditions will be reused first. Otherwise, its own dependency will be loaded and stored in the global cache for later reuse.

This API is generally not called directly by the user, but is used by the build plugin when transforming its own dependencies.

Build Plugin (Use build plugin)
Pure Runtime (Not use build plugin)
import { registerShared, loadShare } from '@module-federation/enhanced/runtime';
import React from 'react';
import ReactDom from 'react-dom';

registerShared({
  react: {
    version: '17.0.0',
    scope: 'default',
    lib: () => React,
    shareConfig: {
      singleton: true,
      requiredVersion: '^17.0.0',
    },
  },
  'react-dom': {
    version: '17.0.0',
    scope: 'default',
    lib: () => ReactDom,
    shareConfig: {
      singleton: true,
      requiredVersion: '^17.0.0',
    },
  },
});

loadShare('react').then((reactFactory) => {
  console.log(reactFactory());
});

If multiple versions of shared are set, by default the loaded one with the highest version is returned. This behavior can be changed by setting extraOptions.resolver:

loadShare('react', {
  resolver: (sharedOptions) => {
    return (
      sharedOptions.find((i) => i.version === '17.0.0') ?? sharedOptions[0]
    );
  },
}).then((reactFactory) => {
  console.log(reactFactory()); // { version: '17.0.0' }
});

loadRemote

This API is used to load a remote module at runtime.

type loadRemote = (remoteNameOrAlias: string) => Promise<RemoteModule>;
Build Plugin (Use build plugin)
Pure Runtime (Not use build plugin)
import { loadRemote } from '@module-federation/enhanced/runtime';

// remoteName + expose
loadRemote('remote/util').then((m) => m.add(1, 2, 3));

// alias + expose
loadRemote('app1/util').then((m) => m.add(1, 2, 3));

preloadRemote

  Type declaration
async function preloadRemote(preloadOptions: Array<PreloadRemoteArgs>){}

type depsPreloadArg = Omit<PreloadRemoteArgs, 'depsRemote'>;
type PreloadRemoteArgs = {
  // Name or alias of the remote to be preloaded
  nameOrAlias: string;
  // The exposes to be preloaded
  // By default, all exposes are preloaded
  // When exposes are provided, only the required exposes will be loaded
  exposes?: Array<string>; // Default request
  // The default is sync, which only loads the synchronous code referenced in expose
  // When set to all, both synchronous and asynchronous references will be loaded
  resourceCategory?: 'all' | 'sync';
  // When no value is configured, all dependencies are loaded by default
  // After configuring dependencies, only the configuration options will be loaded
  depsRemote?: boolean | Array<depsPreloadArg>;
  // When not configured, resources are not filtered
  // After configuration, unnecessary resources will be filtered
  filter?: (assetUrl: string) => boolean;
};

With preloadRemote, you can start preloading module resources at an earlier stage to avoid waterfall requests. preloadRemote can preload:

  • the remote's remoteEntry
  • the remote's expose
  • the remote's synchronous or asynchronous resources
  • the remote's dependent remote resources

preloadRemote waits for all resources involved in the current preload call to finish. If every resource is loaded successfully or hits the cache, the Promise resolves; if any resource fails or times out, the Promise rejects, and the error object carries the resource results of this preload call.

If you only care about the final result, you can use await or .then/.catch directly:

import { preloadRemote } from '@module-federation/enhanced/runtime';

try {
  await preloadRemote([
    {
      nameOrAlias: 'sub1',
      exposes: ['add'],
      resourceCategory: 'all',
    },
  ]);

  console.log('sub1/add preload success');
} catch (error) {
  console.error('sub1/add preload failed', error);
}

If you need to track which specific resources succeeded, failed, timed out, or came from cache, read results from the error object:

type PreloadRemoteError = Error & {
  results?: Array<{
    id: string;
    results: Array<{
      url: string;
      status: 'success' | 'error' | 'timeout' | 'cached';
      resourceType: 'manifest' | 'remoteEntry' | 'js' | 'css';
      error?: unknown;
    }>;
  }>;
};

preloadRemote([
  {
    nameOrAlias: 'sub1',
    exposes: ['add'],
    resourceCategory: 'all',
  },
]).catch((error: PreloadRemoteError) => {
  const failedResources =
    error.results
      ?.flatMap((remoteResult) =>
        remoteResult.results.map((resource) => ({
          id: remoteResult.id,
          ...resource,
        })),
      )
      .filter(
        (resource) =>
          resource.status === 'error' || resource.status === 'timeout',
      ) ?? [];

  failedResources.forEach((resource) => {
    console.error(
      `[preloadRemote] ${resource.id} ${resource.resourceType} failed`,
      resource.url,
      resource.error,
    );
  });
});

When exposes is not specified, the resource id for this preload is remoteName/*. When exposes is specified, the runtime generates resources per expose, and the resource id is remoteName/expose.

Build Plugin (Use build plugin)
Pure Runtime (Not use build plugin)
import { registerRemotes, preloadRemote } from '@module-federation/enhanced/runtime';

registerRemotes([
  {
    name: 'sub1',
    entry: 'http://localhost:2001/mf-manifest.json',
  },
  {
    name: 'sub2',
    entry: 'http://localhost:2002/mf-manifest.json',
  },
  {
    name: 'sub3',
    entry: 'http://localhost:2003/mf-manifest.json',
  },
]);

// preload sub1 module
// filter the resource information that carries ignore in the resource name
// only preload sub-dependent sub1-button module
preloadRemote([
  {
    nameOrAlias: 'sub1',
    filter(assetUrl) {
      return assetUrl.indexOf('ignore') === -1;
    },
    depsRemote: [{ nameOrAlias: 'sub1-button' }],
  },
]);

// preload sub2 module
// preload all exposes under sub2
// preload synchronous and asynchronous resources of sub2
preloadRemote([
  {
    nameOrAlias: 'sub2',
    resourceCategory: 'all',
  },
]);

// preload add expose of sub3 module
preloadRemote([
  {
    nameOrAlias: 'sub3',
    resourceCategory: 'all',
    exposes: ['add'],
  },
]);