Skip to content

Module Federation Example

A minimal example where a remote built with ZNTC is consumed as-is by a standard @module-federation/runtime host, with no separate ZNTC runtime. ZNTC targets the standard Module Federation runtime contract, so the host side carries zero ZNTC dependencies.

Read the Module Federation guide first for concepts, configuration, and limits. This page is the recipe that runs that guide.

remote/
zntc.config.json # mf: name / exposes / shared
src/Button.tsx # the exposed React component (react is shared)
src/index.ts # remote entry (the container is generated from mf.exposes)
host.mjs # consumes the remote via standard @module-federation/runtime

remote/zntc.config.json:

{
"mf": {
"name": "remote_app",
"exposes": { "./Button": "./src/Button.tsx" },
"shared": { "react": { "singleton": true, "requiredVersion": "^19" } }
}
}
  • exposes — modules to expose so other apps can consume them. { publicPath: sourcePath }.
  • shared — dependencies that are not bundled and instead share the single instance the host provides. Required for libraries like React where split instances break hooks. singleton + requiredVersion pins a version contract.

remote/src/Button.tsxreact is declared in mf.shared, so it is not bundled and shares the host’s single instance.

import { useState, createElement } from 'react';
export default function Button() {
const [count, setCount] = useState(0);
return createElement(
'button',
{ onClick: () => setCount(count + 1) },
`remote Button — count: ${count}`,
);
}

remote/src/index.ts is just an entry sentinel — the container (remoteEntry) is generated by ZNTC from mf.exposes.

export const name = 'remote_app';

A remote is a MF remote container, not an app, so build it in core mode rather than app-mode zntc build.

Terminal window
cd remote
zntc --bundle src/index.ts --outdir dist --format=iife \
--public-path=file://$PWD/dist/

Output: the container (remoteEntry) + mf-manifest.json + content-hashed chunks.

host.mjs — no ZNTC dependency, only the standard @module-federation/runtime init / loadRemote.

import * as hostReact from 'react';
import * as mfNs from '@module-federation/runtime';
// @module-federation/runtime is CJS — namespace import, then default
const { init, loadRemote } = mfNs.default ?? mfNs;
// The host registers its own react as shared → the remote's
// shared:{react} reuses this single instance (hooks work).
init({
name: 'host_app',
remotes: [{ name: 'remote_app', entry: 'http://localhost:PORT/index.js' }],
shared: {
react: {
version: '19.0.0',
lib: () => hostReact,
shareConfig: { singleton: true, requiredVersion: '^19' },
},
},
});
// Standard loadRemote pulls the ZNTC remote component dynamically.
const mod = await loadRemote('remote_app/Button');
const Button = mod.default ?? mod;

Node does not support http imports, so this example serves only the remote entry over http and loads chunks from the build-time publicPath (file://dist/) — an “entry=http / chunk=file://” hybrid. In a browser host both are http.

From the repository root, install once:

Terminal window
bun install
bun run --cwd examples/module-federation demo

demo (1) builds remote/ with ZNTC and (2) runs host.mjs. Run them separately:

Terminal window
bun run --cwd examples/module-federation build # zntc → remote/dist
bun run --cwd examples/module-federation start # host.mjs

Expected output:

component: true / shared React singleton: true
element type === Button: true
OK — zntc remote 컴포넌트를 표준 host 가 소비, react 단일 인스턴스 공유
  • host.mjs has no ZNTC dependency — it runs on the standard @module-federation/runtime alone.
  • loadRemote('remote_app/Button') pulls the component from the container ZNTC emitted.
  • The remote’s react is the same instance as the one the host registered (shared singleton holds — the condition for hooks not to break).

Full code lives in examples/module-federation.