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-appv2.0 requireshost-appv1.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! 👇
