Module Federation lets you dynamically share JavaScript code between independently deployed applications.
Here’s a basic Vite setup for Module Federation, demonstrating sharing a simple utility function:
packages/shared-utils/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
react(),
federation({
name: 'shared-utils',
filename: 'remoteEntry.js',
exposes: {
'./utils': './src/index.ts',
},
shared: ['react', 'react-dom'],
}),
],
build: {
target: 'esnext',
minify: false, // Easier to inspect for demo purposes
cssCodeSplit: false,
},
});
packages/shared-utils/src/index.ts
export function greet(name: string): string {
return `Hello, ${name}!`;
}
packages/app-a/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
react(),
federation({
name: 'app-a',
filename: 'remoteEntry.js',
remotes: {
shared: 'http://localhost:5001/assets/remoteEntry.js', // URL to shared-utils remoteEntry
},
shared: ['react', 'react-dom'],
}),
],
build: {
target: 'esnext',
minify: false,
cssCodeSplit: false,
},
});
packages/app-a/src/App.tsx
import React, { Suspense } from 'react';
import './App.css';
const Utils = React.lazy(() => import('shared/utils'));
function App() {
const message = Utils.greet('World'); // Dynamically imported and used
return (
<div className="App">
<header className="App-header">
<h1>App A</h1>
<p>{message}</p>
</header>
</div>
);
}
export default function WrappedApp() {
return (
<Suspense fallback={<div>Loading shared module...</div>}>
<App />
</Suspense>
);
}
To run this:
- Navigate to
packages/shared-utilsand runnpm installthennpm run dev. - Open another terminal, navigate to
packages/app-aand runnpm installthennpm run dev.
You’ll see "App A" displaying "Hello, World!" – the greet function from shared-utils is being used in app-a without being directly bundled into app-a’s build output.
Module Federation fundamentally changes how you think about application boundaries. Instead of a monolithic build or a set of completely disconnected micro-frontends, it creates a shared runtime environment. When app-a requests shared/utils, Vite’s Module Federation plugin intercepts this request at runtime. It then fetches the remoteEntry.js from the shared-utils application (hosted on http://localhost:5001 in this example). This remoteEntry.js acts as a manifest, telling app-a where to find the actual utils module code. The browser downloads this code on demand and makes it available to app-a, allowing it to call Utils.greet. This dynamic loading means app-a doesn’t need to know about the implementation details of shared-utils at build time; it only needs to know the remote entry point and the exposed module name.
The shared array in the federation configuration is critical. When shared-utils exposes react and react-dom, and app-a also declares react and react-dom as shared, Module Federation ensures that only one version of react and react-dom is loaded and used across both applications. The plugin applies a sophisticated version negotiation strategy: if versions are compatible (e.g., 18.2.0 in one app and 18.2.1 in another), the version from the first loaded application (or a specified singleton) is used. This prevents duplicate dependencies and potential runtime conflicts. If versions are incompatible, it can lead to errors.
The remotes configuration in app-a is a map where keys are the identifiers used in import statements (like shared) and values are the URLs pointing to the remote entry point of the federated module. The exposes configuration in shared-utils defines what modules are made available and under what name (e.g., utils exposed as ./utils).
The build.minify: false and build.cssCodeSplit: false in the Vite configs are helpful for debugging and understanding. In production, you’d typically want these enabled. cssCodeSplit: false is often used with Module Federation to ensure that shared CSS isn’t split into chunks that might not be loaded correctly by dependent applications.
One of the most powerful, yet often overlooked, aspects of Module Federation is its ability to handle application bootstrapping. When you have multiple applications that depend on shared modules, the order in which they load and initialize can matter. The shared configuration doesn’t just manage dependency versions; it also dictates which application’s version of a shared dependency is prioritized. If app-a loads first and declares react as shared, and then app-b loads and also declares react, app-a’s react will likely be used by app-b. This can be leveraged to ensure a consistent UI framework version across your entire micro-frontend architecture.
The next step is exploring how to manage version conflicts and more complex dependency graphs between federated modules.