Taming the Beast: Mastering Micro-Frontends with Webpack Module Federation & Next.js


Introduction: Why Micro-Frontends Matter Now
Modern web apps are evolving into sprawling ecosystems. Monolithic React codebases buckle under the weight of feature creep, conflicting team priorities, and deployment bottlenecks. Enter micro-frontends—a paradigm that decomposes your UI into independent, team-owned "apps within an app." But how do you glue them together without chaos?
The answer: Webpack Module Federation + Next.js. Let’s build a scalable architecture that balances autonomy with cohesion.
1. Module Federation 101: The Glue That Holds It All Together
Webpack Module Federation (WMF) lets apps dynamically load code from remote sources at runtime. Think of it as a microservice architecture for your frontend.
Real-World Example:
A travel booking platform splits its UI into:
search-app
(Next.js, Team A)booking-app
(React + Vite, Team B)user-profile-app
(Remix, Team C)
Each team deploys independently, but WMF stitches them into a seamless experience.
Code Snippet: Basic WMF Setup
// webpack.config.js (Host App)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host_app',
remotes: {
search: 'search_app@https://cdn.your-domain.com/remoteEntry.js',
// include more.
},
shared: ['react', 'react-dom'], // Avoid duplicate dependencies
}),
],
};
Insight: Use shared
to avoid loading React twice. Version mismatches? Webpack’s singleton: true
enforces a single instance.
2. Next.js as the Orchestrator: Supercharge Your Host App
Next.js isn’t just for SSR—it’s a powerhouse for micro-frontend routing and performance.
Real-World Pattern:
Host App: Next.js handles routing, authentication, and global state.
Remotes: Feature apps (React, Vue, etc.) lazy-loaded via dynamic imports.
Code Snippet: Dynamic Remote Loading
// pages/index.tsx (Host App)
import dynamic from 'next/dynamic';
const SearchApp = dynamic(
() => import('search_app/SearchModule'),
{ loading: () => <p>Loading search...</p>, ssr: false }
);
const HomePage = () => (
<div>
<Header />
<SearchApp /> {/* Rendered from remote! */}
</div>
);
Insight: Disable SSR for remotes if they’re client-side only. Next.js 13+ with React Server Components? Plan carefully—not all remotes play nice with RSC yet.
3. Deployment: CI/CD Pipelines That Don’t Collide
Strategy 1: Independent Deployments
Each app has its own CI/CD pipeline.
Use semantic versioning for shared libraries.
Strategy 2: Coordinated Releases
Deploy the host app after remotes (e.g.,
booking-app
v2.0 requireshost-app
v1.5).Tools like Azure DevOps or GitHub Actions can automate dependency checks.
Real-World Nightmare (and Fix):
A retail app’s checkout page can break because the host app could cache an old remoteEntry.js.
Solution: Use CDNs with immutable filenames (e.g., remoteEntry-[hash].js
).
4. Taming Shared Dependencies
Problem: Multiple React instances = silent crashes, hooks hell.
Solution: Enforce dependency alignment:
// In all webpack.config.js files
shared: {
react: { singleton: true, requiredVersion: '^18.2.0' },
'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
},
Pro Tip: Audit dependencies with npm ls react
across all apps.
5. CSS Collisions: The Silent Killer
Problem: A .button
class in search-app
overrides styles in booking-app
.
Solutions:
Scoped CSS: Use CSS Modules or styled-components.
Atomic CSS: Adopt Tailwind with strict naming conventions.
Shadow DOM: Extreme isolation for legacy apps.
Real-World Fix:
A fintech app used CSS-in-JS (Emotion) with unique classname hashes to eliminate collisions.
6. Cross-App State Sync: Beyond React Context
Problem: How does search-app
notify booking-app
that a date was selected?
Solution 1: Event Bus
// shared/eventBus.js
export const bus = new EventEmitter();
// In search-app
bus.emit('dateSelected', payload);
// In booking-app
bus.on('dateSelected', handleDate);
Solution 2: Zustand (Global Store)
// shared/store.js
import { create } from 'zustand';
export useSharedStore = create((set) => ({
selectedDate: null,
setDate: (date) => set({ selectedDate: date }),
}));
Insight: Avoid over-sharing. Not every state needs to be global!
7. Testing: Don’t Let the House of Cards Collapse
Contract Testing: Ensure remotes comply with host API expectations (use Pact).
Integration Tests: Run Cypress against a stitched production build.
Pro Tip: Mock remotes in the host’s Jest setup for faster unit tests.
Conclusion: Start Small, Scale with Confidence
Micro-frontends aren’t a silver bullet—they’re a tradeoff. Use them when:
Teams outgrow monolithic workflows.
Tech diversity is unavoidable (React + Angular + Vue).
You need incremental upgrades (migrate legacy code piecemeal).
Final Wisdom: "Micro-frontends solve organizational problems first, technical ones second."
🚀 Ready to Dive Deeper?
Got war stories or questions? Share them below! 👇
Subscribe to my newsletter
Read articles from JealousGx directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

JealousGx
JealousGx
Hello, I'm a highly skilled full stack web developer with a rich background in creating and maintaining dynamic websites that drive online engagement and brand awareness. My expertise extends beyond WordPress to include a versatile skill set.