Shared Dependencies & Singleton

Published: April 14, 2026 · 12 min read

Module Federation Shared Dependencies and Singleton Pattern

You've configured host and remote apps with module federation shared dependencies — the host consumes remotes, each remote exposes components, and remoteEntry.js ties it together. But one section of every webpack config was copy-pasted without question: the shared object. What does singleton: true actually do? What happens when one remote has React 18.2 and another has React 18.3? And why does removing one shared dependency crash the entire application with an "Invalid hook call" error?

This article explains the module federation singleton pattern, version negotiation, requiredVersion vs strictVersion vs eager, what you should and should not share, and how to debug shared dependency issues at runtime.

This is Article 14 in the Micro Frontend Architecture series. If you haven't set up host and remote apps yet, start with Module Federation Host and Remote Apps.

Module Federation shared dependencies diagram showing singleton pattern preventing duplicate React instances across micro frontends

The Duplicate Bundle Problem

Without the shared configuration, every MFE bundles its own copy of every dependency. If your host and three remotes all use React, ReactDOM, and React Router — you load four copies of each library.

bundle-analysis-without-sharing.txt
Without shared dependencies — every MFE bundles its own copy:

Host App Bundle:
  ├── react@18.3.1            (135 KB gzipped)
  ├── react-dom@18.3.1        (42 KB gzipped)
  ├── react-router-dom@7.1.5  (15 KB gzipped)
  └── host app code            (50 KB)
  Total: ~242 KB

Products Remote Bundle:
  ├── react@18.3.1            (135 KB)  ← DUPLICATE
  ├── react-dom@18.3.1        (42 KB)   ← DUPLICATE
  ├── react-router-dom@7.1.5  (15 KB)   ← DUPLICATE
  └── products app code        (80 KB)
  Total: ~272 KB

Orders Remote Bundle:
  ├── react@18.3.1            (135 KB)  ← DUPLICATE
  ├── react-dom@18.3.1        (42 KB)   ← DUPLICATE
  ├── react-router-dom@7.1.5  (15 KB)   ← DUPLICATE
  └── orders app code          (60 KB)
  Total: ~252 KB

Inventory Remote Bundle:
  ├── react@18.3.1            (135 KB)  ← DUPLICATE
  ├── react-dom@18.3.1        (42 KB)   ← DUPLICATE
  ├── react-router-dom@7.1.5  (15 KB)   ← DUPLICATE
  └── inventory app code       (55 KB)
  Total: ~247 KB

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total JavaScript loaded by browser: ~1,013 KB
React duplicated: 4 copies × 177 KB = 708 KB
Wasted bandwidth: 531 KB (3 unnecessary copies)

That is 531 KB of duplicate JavaScript the browser downloads, parses, and executes for no reason. But the performance waste is not even the worst problem. Multiple React instances break hooks. React's useState, useEffect, and useContext hooks maintain internal state in a module-level registry. When two copies of React exist, a component rendered by React instance #1 tries to read hooks from React instance #2 — and React throws the infamous "Invalid hook call" error.

console-error.txt
// Console error when multiple React instances exist:

Uncaught Error: Invalid hook call. Hooks can only be called
inside of the body of a function component.

This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

// Cause #3 is the Module Federation culprit.
// Two React instances = two separate hook registries.
// A component rendered with React #1 cannot use hooks from React #2.
//
// Fix: Add singleton: true to BOTH host AND remote shared configs.

The shared configuration in ModuleFederationPlugin (opens in a new tab) solves both problems — it eliminates duplicate bundles and ensures a single instance of critical libraries.

bundle-analysis-with-sharing.txt
With shared dependencies — one copy loaded once, used by all MFEs:

Shared Module Pool (loaded once by Webpack):
  ├── react@18.3.1            (135 KB)  ← SINGLE INSTANCE
  ├── react-dom@18.3.1        (42 KB)   ← SINGLE INSTANCE
  └── react-router-dom@7.1.5  (15 KB)   ← SINGLE INSTANCE
  Total shared: ~192 KB

Host App Bundle:
  └── host app code            (50 KB)

Products Remote Bundle:
  └── products app code        (80 KB)

Orders Remote Bundle:
  └── orders app code          (60 KB)

Inventory Remote Bundle:
  └── inventory app code       (55 KB)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total JavaScript loaded by browser: ~437 KB
React loaded: 1 copy × 177 KB = 177 KB
Bandwidth saved: 576 KB (57% reduction)

How Module Federation Shared Dependencies Work

The shared object tells Webpack which dependencies should be loaded once and reused across all federated modules. When the host app loads, it initializes a shared scope — a registry of available modules and their versions. When a remote loads, its remoteEntry.js checks this scope before bundling its own copy.

version-negotiation-flow.txt
Shared dependency version negotiation flow:

1. Host app loads first
   └── Initializes shared scope with its versions:
       react@18.3.1, react-dom@18.3.1, @myapp/store@0.0.1

2. Browser fetches Products/remoteEntry.js
   └── remoteEntry.js registers Products' shared requirements:
       react@^18.3.1, react-dom@^18.3.1, @myapp/store@0.0.1

3. Webpack runs version negotiation for each shared module:
   ┌─────────────────────────────────────────────────────────┐
   │ Module: react                                           │
   │ Host version: 18.3.1                                    │
   │ Remote requires: ^18.3.1                                │
   │ Singleton: true                                         │
   │                                                         │
   │ Decision: ✅ Host version satisfies ^18.3.1             │
   │ Action: Remote uses Host's React instance               │
   └─────────────────────────────────────────────────────────┘
   ┌─────────────────────────────────────────────────────────┐
   │ Module: @myapp/store                                    │
   │ Host version: 0.0.1                                     │
   │ Remote requires: 0.0.1                                  │
   │ Singleton: true                                         │
   │                                                         │
   │ Decision: ✅ Exact version match                        │
   │ Action: Remote uses Host's store instance                │
   └─────────────────────────────────────────────────────────┘

4. Products remote code executes
   └── All imports resolve to the shared instances from step 1
   └── No duplicate modules — singleton enforced

The negotiation happens automatically during the async boundary created by the bootstrap pattern. This is why import("./bootstrap") is required — it gives Webpack the async gap to load all remoteEntry.js files and resolve shared versions before any application code runs.

The shared config does NOT change between environments. Unlike remotes (where URLs differ between localhost:4002 and /products/), the shared object is identical in local development and production. The same version negotiation rules apply in both environments.

The Singleton Pattern

The most important property in the shared config is singleton: true. Without it, Webpack may create separate instances of a module even when the versions match. With it, Webpack guarantees exactly one instance exists in the browser.

singleton-enabled.js
// With singleton: true — ONE instance shared across all MFEs
shared: {
  react: {
    singleton: true,
    requiredVersion: "^18.3.1",
  },
}

// Runtime behavior:
// 1. Host loads first → Webpack initializes React 18.3.1
// 2. Products remote starts loading
// 3. Webpack checks: "Is React already in the shared scope?"
// 4. Yes → Products reuses the SAME React instance from Host
// 5. Orders remote starts loading
// 6. Webpack checks again → reuses the SAME instance
//
// Result: 1 React instance in memory
// All hooks work, shared context works, state is consistent

Module Federation singleton pattern memory diagram comparing single React instance versus duplicate instances across micro frontends

The singleton pattern is mandatory for any library that maintains module-level state — React (hook registry), ReactDOM (reconciler), React Router (navigation context), Redux (store instance), and any custom shared package that holds state. Without singleton, each MFE gets its own isolated instance and cross-MFE state sharing breaks silently.

⚠️

Both host AND remote must declare singleton: true. If only the host sets singleton but the remote does not, the remote may still bundle its own copy. The singleton flag must be declared on both sides for Webpack to enforce the single-instance constraint.

requiredVersion and Version Negotiation

The requiredVersion property tells Webpack what version range a module expects. During negotiation, Webpack checks whether the loaded version satisfies each app's requiredVersion constraint. There are four strategies for setting this value.

required-version-strategies.js
// Strategy 1: Read from package.json (RECOMMENDED for app dependencies)
shared: {
  react: {
    singleton: true,
    requiredVersion: dependencies.react,  // resolves to "^18.3.1"
  },
}
// ✅ Stays in sync with your installed version automatically
// ✅ No manual updates needed when upgrading


// Strategy 2: Hardcoded semver range (when package.json differs)
shared: {
  "@reduxjs/toolkit": {
    singleton: true,
    requiredVersion: "^2.6.0",
  },
}
// ✅ Explicit — clear about expected version
// ⚠️ Must update manually when upgrading the package


// Strategy 3: Exact version for custom monorepo packages
shared: {
  "@myapp/store": {
    singleton: true,
    requiredVersion: "0.0.1",
  },
}
// ✅ Strict — host and remote MUST use the exact same version
// ✅ Best for monorepo packages where version is always locked


// Strategy 4: requiredVersion: false (Next.js pattern)
shared: {
  react: {
    singleton: true,
    requiredVersion: false,
  },
}
// ✅ Skips version check entirely — framework manages the version
// ⚠️ Only use when the framework guarantees version consistency

When the loaded version does not satisfy requiredVersion, Webpack logs a console warning: "Unsatisfied version X of shared singleton module Y (required Z)". The app continues running with the mismatched version. If you want mismatches to crash the app immediately (so you catch them in development), use strictVersion: true.

strict-version-example.js
// strictVersion: true — RUNTIME ERROR on version mismatch (not just a warning)

// Without strictVersion (default behavior):
shared: {
  "@myapp/store": {
    singleton: true,
    requiredVersion: "1.0.0",
    // strictVersion defaults to false
  },
}
// Host has @myapp/store@1.0.0, Remote has @myapp/store@1.0.1
// → Console WARNING: "Unsatisfied version 1.0.1 of shared singleton
//   module @myapp/store (required =1.0.0)"
// → App continues running — may have subtle bugs you miss


// With strictVersion: true:
shared: {
  "@myapp/store": {
    singleton: true,
    strictVersion: true,
    requiredVersion: "1.0.0",
  },
}
// Host has @myapp/store@1.0.0, Remote has @myapp/store@1.0.1
// → RUNTIME ERROR thrown immediately
// → App crashes — you KNOW there is a version mismatch
// → Forces you to fix it before deployment

Use strictVersion: true for custom monorepo packages where any version difference signals a real problem. Leave it off for third-party packages like React where minor version differences (18.3.0 vs 18.3.1) are intentionally backward-compatible.

The eager Option and the Bootstrap Pattern

The eager property controls whether a shared module is included in the initial bundle or loaded asynchronously. In React apps with Webpack, eager is almost never needed because the async bootstrap pattern handles the timing. In Next.js, eager: false is critical because Next.js has its own module loading system.

eager-consumption-error.js
// ERROR: "Shared module is not available for eager consumption"

// This happens when shared dependencies are used BEFORE federation negotiation.

// ❌ WRONG — Direct entry point
// apps/host/src/index.js
import React from "react";       // ← React needed IMMEDIATELY
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")).render(<App />);
// Webpack has NOT loaded remoteEntry.js files yet
// Shared dependencies have NOT been negotiated
// Result: "Shared module is not available for eager consumption"


// ✅ CORRECT — Async bootstrap pattern
// apps/host/src/index.js
import("./bootstrap");            // ← Async import creates the negotiation gap

// apps/host/src/bootstrap.js
import React from "react";       // ← React is now available after negotiation
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")).render(<App />);
// Webpack loads remoteEntry.js during the async gap
// Shared modules are resolved before bootstrap.js executes

If you see the "Shared module is not available for eager consumption" error, it means your entry point is importing shared modules synchronously before federation negotiation completes. The fix is always the async bootstrap pattern — never set eager: true as a workaround, because that bundles the shared module into the initial chunk and defeats the purpose of sharing.

Host vs Remote — Shared Config Differences

The host and remote declare the same shared modules but the host typically lists more dependencies than each individual remote. This is because the host is the orchestrator — it provides shared instances for every library used anywhere in the application.

apps/host/webpack.config.js
// apps/host/webpack.config.js — Host shared configuration
const { ModuleFederationPlugin } = require("webpack").container;
const dependencies = require("./package.json").dependencies;

new ModuleFederationPlugin({
  name: "Host",
  remotes: {
    Products: "Products@https://localhost:4002/remoteEntry.js",
    Orders: "Orders@https://localhost:4004/remoteEntry.js",
    Inventory: "Inventory@https://localhost:4003/remoteEntry.js",
    Settings: "Settings@https://localhost:4009/remoteEntry.js",
    Onboarding: "Onboarding@https://localhost:4001/remoteEntry.js",
  },
  shared: {
    // Core React — MUST be singleton to prevent "Invalid hook call"
    react: {
      singleton: true,
      requiredVersion: dependencies.react,
    },
    "react-dom": {
      singleton: true,
      requiredVersion: dependencies["react-dom"],
    },
    // Routing — one router context needed across all MFEs
    "react-router-dom": {
      singleton: true,
      requiredVersion: dependencies["react-router-dom"],
    },
    // State management — one Redux store instance for shared state
    "@reduxjs/toolkit": {
      singleton: true,
      requiredVersion: "^2.6.0",
    },
    "react-redux": {
      singleton: true,
      requiredVersion: "^9.2.0",
    },
    // Custom shared package — monorepo's shared store
    "@myapp/store": {
      singleton: true,
      requiredVersion: "0.0.1",
    },
  },
})

Notice the differences: the host declares @reduxjs/toolkit and react-redux because it creates and provides the Redux store. The Products remote does not directly import these — it accesses Redux through @myapp/store, which re-exports the configured store. The Onboarding remote adds swiper because it uses a carousel component that another remote also needs.

Rule of thumb: Only declare a dependency in shared if your code directly imports it. If you access a library indirectly through a shared package, the shared package handles the sharing.

React vs Next.js — Shared Config Differences

When your host uses Next.js instead of React with Webpack, the shared configuration changes significantly. Next.js uses NextFederationPlugin (opens in a new tab) instead of ModuleFederationPlugin, and the shared properties use different defaults.

next.config.js — Next.js shared configuration
// next.config.js — Next.js shared config with NextFederationPlugin
const { NextFederationPlugin } = require("@module-federation/nextjs-mf");

module.exports = {
  webpack(config, options) {
    config.plugins.push(
      new NextFederationPlugin({
        name: "Host",
        remotes: {
          Products: options.isServer
            ? "Products@http://localhost:3001/_next/static/ssr/remoteEntry.js"
            : "Products@http://localhost:3001/_next/static/chunks/remoteEntry.js",
        },
        shared: {
          // React — requiredVersion: false lets Next.js manage versions
          react: {
            singleton: true,
            requiredVersion: false,
            eager: false,
          },
          "react-dom": {
            singleton: true,
            requiredVersion: false,
            eager: false,
          },
          // Redux — same pattern as React
          "react-redux": {
            singleton: true,
            requiredVersion: false,
            eager: false,
          },
          "@reduxjs/toolkit": {
            singleton: true,
            requiredVersion: false,
            eager: false,
          },
          // Custom packages — strictVersion enforces exact match
          "@myapp/store": {
            singleton: true,
            strictVersion: true,
            requiredVersion: "1.0.0",
          },
          "@myapp/api": {
            singleton: true,
            strictVersion: true,
            requiredVersion: "1.0.0",
          },
          "@myapp/seo": {
            singleton: true,
            strictVersion: true,
            requiredVersion: "1.0.0",
          },
        },
      })
    );
    return config;
  },
};

Key Differences

PropertyReact (Webpack MFP)Next.js (NextFederationPlugin)
singletontrue (always for React/Redux)true (always for React/Redux)
requiredVersiondependencies.react or "^18.3.1"false (Next.js manages versions)
strictVersionNot commonly usedtrue for custom packages (@myapp/*)
eagerNot needed (bootstrap pattern handles)false (prevents SSR hydration issues)
Custom packages@myapp/store with exact version@myapp/store + @myapp/api + @myapp/seo
Config filewebpack.config.jsnext.config.js
Version sourcepackage.json dependenciesfalse — framework-managed

The biggest difference is eager: false in Next.js. React apps handle this through the bootstrap pattern (import("./bootstrap")), but Next.js has its own module loading mechanism — setting eager: false ensures shared modules are loaded through federation's async resolution rather than bundled into Next.js' initial chunks. Setting eager: true in Next.js causes hydration mismatches and SSR errors.

The second difference is strictVersion: true for custom packages. The Next.js codebase uses this for @myapp/store, @myapp/api, and @myapp/seo — monorepo packages where version consistency is critical for SSR/client rendering to produce identical output.

What You Should Share

Not every dependency belongs in the shared config. Share too little and you get duplicate instances and broken hooks. Share too much and you slow down federation negotiation and prevent tree-shaking.

LibraryShare?Singleton?Reason
reactYesYesMultiple instances break hooks and context
react-domYesYesMust match react version — one renderer per app
react-router-domYesYesOne router context needed for URL sync across MFEs
@reduxjs/toolkitYesYesOne Redux store instance for shared state
react-reduxYesYesProvider context must match the store
@myapp/storeYesYesCustom shared package — all MFEs read the same store
axiosMaybeNoStateless — duplicates waste bytes but do not break logic
lodashNoNoTree-shaken per MFE — sharing prevents tree-shaking
date-fnsNoNoEach MFE imports different functions — sharing bloats all
tailwindcssNoNoCSS framework — not a JS runtime dependency
MFE-specific UI libsNoNoOnly used by one MFE — zero benefit from sharing

The rule is simple: share libraries that maintain state or context. React, ReactDOM, Router, Redux, and custom stores must be singletons because multiple instances break cross-MFE communication. Stateless utility libraries (lodash, date-fns, axios) can safely be duplicated — the extra bytes are a minor cost compared to the complexity of managing shared versions.

What NOT to Share

These categories of libraries should never go in the shared config:

  1. Tree-shakeable utilities — lodash, date-fns, ramda. If the Products MFE imports format from date-fns and the Orders MFE imports addDays, sharing forces both MFEs to load the entire library instead of just the functions they use.

  2. CSS-only dependencies — Tailwind CSS, styled-components (at build time), PostCSS plugins. These are build-time tools that produce CSS output. They are not runtime JavaScript dependencies.

  3. MFE-specific libraries — A charting library used only in the Analytics MFE, or a rich text editor used only in Content. Sharing these adds them to the global scope where no other MFE benefits.

  4. Dev-only dependencies — ESLint, Prettier, testing libraries. These never run in the browser — sharing them is meaningless.

⚠️

Sharing axios is debatable. Axios is stateless — duplicate copies do not break functionality. But if you attach interceptors (auth headers, error handling) to a shared axios instance in the host, you want all MFEs to use that same configured instance. In that case, create a @myapp/api shared package that exports the configured axios client, and share that package as a singleton.

Debugging Shared Dependencies

When shared dependencies misbehave — hooks break, state does not sync, or warnings fill the console — you can inspect the shared scope at runtime.

browser-devtools-console.js
// Inspect shared module resolution at runtime
// Open browser DevTools Console and run:

// 1. List all shared scopes
console.log(__webpack_share_scopes__);

// 2. Check the default scope (where all shared modules live)
console.log(__webpack_share_scopes__.default);

// 3. See which version of React was loaded and by whom
console.log(__webpack_share_scopes__.default.react);
// Output: {
//   "18.3.1": {
//     get: ƒ,          — factory function to get the module
//     loaded: 1,       — 1 = loaded, 0 = not yet loaded
//     from: "Host",    — which app provided this version
//     eager: false      — whether it was eagerly consumed
//   }
// }

// 4. Check a custom shared module
console.log(__webpack_share_scopes__.default["@myapp/store"]);
// Output: { "0.0.1": { get: ƒ, loaded: 1, from: "Host" } }

// 5. Verify singleton is working correctly
// GOOD — single version key means singleton is enforced:
// { "18.3.1": { loaded: 1 } }
//
// BAD — multiple version keys means singleton FAILED:
// { "18.2.0": { loaded: 1 }, "18.3.1": { loaded: 1 } }
// → Two copies of React are in memory — hooks will break

Module Federation shared dependencies debugging guide showing DevTools console inspection of webpack share scopes

Common issues and fixes:

SymptomCauseFix
"Invalid hook call"Multiple React instancesAdd singleton: true to BOTH host and remote
"Shared module not available for eager consumption"Missing async bootstrapUse import("./bootstrap") entry pattern
"Unsatisfied version X of shared singleton module Y"Version mismatchAlign versions in all package.json files
Remote loads but state is emptyShared store not declaredAdd @myapp/store to remote's shared config
Hooks work locally but break in productionDifferent shared configs between environmentsVerify shared config is identical in both builds

What's Next?

← Back to Module Federation Host and Remote Apps

Continue to Dynamic Remote Loading →


Frequently Asked Questions

What does singleton: true do in Module Federation shared config?

singleton: true ensures that only one instance of a shared module exists in the browser at any time. When the host loads React 18.3.1 first, every remote that also declares react as a singleton shared dependency reuses that exact same instance instead of loading its own copy. This prevents the "Invalid hook call" error caused by multiple React instances, ensures hooks work across MFE boundaries, and keeps context providers (Redux, Router) consistent across the entire application.

What happens if two MFEs have different React versions with singleton: true?

When singleton: true is set and two MFEs declare different React versions, Webpack picks the highest version that satisfies all requiredVersion constraints. If the host has React 18.3.1 and a remote requires ^18.2.0, the host's 18.3.1 satisfies ^18.2.0 — so both use 18.3.1 with no issue. If the versions are incompatible (e.g., host has 17.0.2 and remote requires ^18.0.0), Webpack logs a warning and uses the first loaded version. With strictVersion: true, this becomes a runtime error.

Should I share all dependencies in Module Federation?

No. Only share libraries that must exist as a single instance — React, ReactDOM, react-router-dom, Redux, and custom shared packages like your monorepo store. Libraries like lodash and date-fns should not be shared because they are stateless (duplicates do not cause bugs) and tree-shakeable (sharing forces the entire library to load even if each MFE only uses a few functions). Sharing everything increases the initial shared scope size and slows down federation's version negotiation.

What is the difference between requiredVersion and strictVersion?

requiredVersion declares what version range a module expects (e.g., "^18.3.1"). If the loaded version does not satisfy this range, Webpack logs a console warning but the app continues running. strictVersion: true upgrades that warning to a runtime error — the app crashes immediately when a version mismatch is detected. Use requiredVersion alone for third-party packages where minor differences are safe. Use strictVersion: true for custom monorepo packages where even a patch difference can introduce breaking changes.

Why does the host app share more dependencies than remote apps?

The host declares every shared dependency used across the application — React, ReactDOM, Router, Redux Toolkit, react-redux, and custom packages. Remote apps only declare dependencies they directly import in their own code. A remote might not import @reduxjs/toolkit directly because it accesses Redux through a shared store package. The remote still uses Redux at runtime — but through the shared store which re-exports the configured store instance.

How do I debug shared dependency issues in Module Federation?

Open DevTools Console and inspect __webpack_share_scopes__.default — this object contains every shared module, its loaded version, which app provided it, and whether it was eagerly consumed. For a healthy setup, each module key should have exactly one version entry. If you see two version keys for react, singleton is not enforced — check that both host and remote declare singleton: true. Common warnings: "Unsatisfied version" (version mismatch) and "Shared module is not available for eager consumption" (missing async bootstrap pattern).