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

JealousGxJealousGx
4 min read

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 requires host-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! 👇

0
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.