The Ultimate Guide: Bootstrapping Microfrontends with Rsbuild, React, and Module Federation 2.0
Have you ever stared at a massive, monolithic frontend codebase and thought, "There has to be a better way"? Welcome to the world of Microfrontends! By decoupling your massive frontend into smaller, independently developed, and deployed applications, you unlock faster build times, independent team workflows, and a more scalable architecture.
However, setting up the foundation for microfrontends from scratch can feel like navigating a minefield of configuration files and dependency conflicts. Not anymore!
In this comprehensive guide, we are going to build a blazing-fast, modern microfrontend architecture using the ultimate trifecta:
- React 19
- React Router 7
- Rsbuild 1.x.x combined with Module Federation 2.0
By the end of this tutorial, you will have bootstrapped a working Provider (Remote) app that exposes components, and a Shell (Host) app that gracefully consumes them.
Note: This tutorial assumes you possess basic terminal knowledge, are running Node.js (v22 or higher), and use
npm.
🏗️ Step 1: Setting Up the Workspace
Let's create a directory to hold both of our applications:
mkdir my-mfe-workspace
cd my-mfe-workspace
# Create the two application directories
mkdir app1 shell
📦 Step 2: Bootstrapping the Provider (Remote) App (app1)
The Provider app (often called a Remote) will expose a React component that the Shell app can consume.
2.1 Initialize and Install Dependencies
Navigate to your app1 directory:
cd app1
npm init -y
Now, let's install the exact packages you need. We will install React 19 and React Router 7 as dependencies, and the required Rsbuild tools as devDependencies.
# Core Dependencies
npm install react@19 react-dom@19 react-router-dom@7
# Dev Dependencies (Build Tools & Types)
npm install -D @rsbuild/core@1.7.0 @rsbuild/plugin-react@1.4.0 @module-federation/rsbuild-plugin@2.1.0 typescript @types/react @types/react-dom
2.2 Configure Rsbuild
Create an rsbuild.config.ts file in the root of app1:
// app1/rsbuild.config.ts
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';
export default defineConfig(({ env }) => ({
plugins: [
pluginReact(),
pluginModuleFederation({
name: 'app1',
exposes: {
'./App': './src/federation.tsx'
},
dts: false,
dev: {
disableLiveReload: false,
},
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
'react-router-dom': { singleton: true, requiredVersion: '^7.0.0' },
},
}),
],
server: {
port: 3002,
cors: true,
},
output: {
assetPrefix: env === 'production' ? '/my-remote-app1/' : '/',
},
}));
2.3 Create the Source Files
We need a few files to make this a working React application. First, let's create the required directory structure:
mkdir src
touch index.html src/main.tsx src/bootstrap.tsx src/App.tsx src/federation.tsx tsconfig.json
tsconfig.json (Base Typescript Config):
{
"compilerOptions": {
"target": "ES2022",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true
},
"include": ["src"]
}
index.html (The HTML Template):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>App 1 (Remote)</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
src/App.tsx (The Main Component):
import React from 'react';
const App = () => {
return (
<div style={{ border: '2px dashed blue', padding: '20px', margin: '20px' }}>
<h2>Hello from App 1 (Remote)</h2>
<p>This is a microfrontend exposed via Module Federation 2.0!</p>
</div>
);
};
export default App;
src/federation.tsx (The Exposed Entry Point):
Note: We highly recommend exposing a file separated from the main root render logic to ensure clean imports for the Shell app.
export { default } from './App';
src/bootstrap.tsx (The App Initializer):
Module Federation requires an async boundary to load shared dependencies. bootstrap.tsx provides this boundary.
import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
const container = document.getElementById('root');
if (container) {
const root = createRoot(container);
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
}
src/main.tsx (The Async Entry):
import('./bootstrap');
Add these scripts to your app1/package.json:
"scripts": {
"start": "rsbuild dev",
"build": "rsbuild build"
}
🚢 Step 3: Bootstrapping the Shell (Host) App
The Shell application orchestrates navigation and dynamically imports the Remote component.
3.1 Initialize and Install Dependencies
Navigate to your shell directory:
cd ../shell
npm init -y
Install the exact same dependencies as the Provider app to ensure our shared constraints (^19.0.0, ^7.0.0) match perfectly:
npm install react@19 react-dom@19 react-router-dom@7
npm install -D @rsbuild/core@1.7.0 @rsbuild/plugin-react@1.4.0 @module-federation/rsbuild-plugin@2.1.0 typescript @types/react @types/react-dom
3.2 Configure Rsbuild
Create rsbuild.config.ts in the root of shell:
// shell/rsbuild.config.ts
import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { pluginModuleFederation } from "@module-federation/rsbuild-plugin";
const createMfUrl = (name: string, env: string, port: number) =>
env === "production"
? `/my-remote-${name}/mf-manifest.json`
: `http://localhost:${port}/mf-manifest.json`;
const sharedPackageConfig = {
singleton: true,
eager: true,
};
export default defineConfig(({ env }) => ({
plugins: [
pluginReact(),
pluginModuleFederation({
name: "shell",
dts: false,
shareStrategy: "loaded-first",
remotes: {
app1: `app1@${createMfUrl("app1", env, 3002)}`,
},
shared: {
react: { ...sharedPackageConfig, requiredVersion: "^19.0.0" },
"react-dom": { ...sharedPackageConfig, requiredVersion: "^19.0.0" },
"react-router-dom": { ...sharedPackageConfig, requiredVersion: "^7.0.0" },
},
}),
],
server: {
port: 3000,
historyApiFallback: true, // Crucial for React Router client-side routing
},
}));
3.3 Create the Source Files
Like the provider app, set up the layout:
mkdir src
touch index.html src/main.tsx src/bootstrap.tsx src/App.tsx tsconfig.json src/global.d.ts
Copy the same tsconfig.json and index.html from app1 into the shell folder, but optionally update the title in index.html to <title>Microfrontend Shell</title>.
src/global.d.ts (Silence Typescript errors for remote modules):
declare module 'app1/App' {
const App: React.ComponentType;
export default App;
}
src/App.tsx ( The Host Routing Logic ):
import React, { lazy, Suspense } from 'react';
import { Routes, Route, Link } from 'react-router-dom';
// Dynamically import the remote!
const RemoteApp1 = lazy(() => import('app1/App'));
const ShellLayout = () => {
return (
<div style={{ fontFamily: 'system-ui, sans-serif' }}>
<nav style={{ padding: '20px', background: '#333', color: 'white' }}>
<ul style={{ display: 'flex', gap: '20px', listStyle: 'none', margin: 0, padding: 0 }}>
<li><Link to="/" style={{ color: 'white', textDecoration: 'none' }}>Home</Link></li>
<li><Link to="/app1" style={{ color: 'white', textDecoration: 'none' }}>App 1</Link></li>
</ul>
</nav>
<main style={{ padding: '20px' }}>
<Suspense fallback={<h2>Loading Microfrontend...</h2>}>
<Routes>
<Route path="/" element={<h1>Welcome to the Microfrontend Shell</h1>} />
<Route path="/app1/*" element={<RemoteApp1 />} />
<Route path="*" element={<h2>404 - Not Found</h2>} />
</Routes>
</Suspense>
</main>
</div>
);
};
export default ShellLayout;
src/bootstrap.tsx (The Shell Initializer):
import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
const container = document.getElementById('root');
if (container) {
const root = createRoot(container);
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
}
src/main.tsx (The Async Entry):
import('./bootstrap');
Add the scripts to your shell/package.json:
"scripts": {
"start": "rsbuild dev",
"build": "rsbuild build"
}
🚀 Step 4: Run It!
You are now ready to test your setup. Open two separate terminal windows.
Terminal 1 (Provider/App1):
cd app1
npm start
Terminal 2 (Host/Shell):
cd shell
npm start
Navigate your browser to http://localhost:3000. You will see the Shell navigation bar. Click on App 1, and you will see your remotely loaded Module Federation application rendered seamlessly via React Router!
The "Async Margin" (Why main.tsx and bootstrap.tsx?)
You'll notice we split the entry into main.tsx doing a dynamic import('./bootstrap') in both applications. This is mandatory in Webpack/Rsbuild Module Federation.
Because react and react-dom are defined as shared dependencies, Rsbuild needs an asynchronous rendering boundary to pause script execution, look up the module federation manifests, and download the shared react packages before executing the React application code. If you bypass this, you will encounter immediate errors complaining that react was not loaded before usage!
Top comments (0)