Shared Dependency Isolation: Multiple Share Scopes
In Module Federation, shared dependencies are registered into the default Share Scope by default. A single Scope is often not enough when:
- You want to isolate part of your shared dependencies from the default pool (for example, running two React ecosystems side-by-side, gradual upgrades, or domain isolation in micro-frontends).
- You want the same package to use different versions or strategies in different domains, while still being shared within each domain (singleton/reuse still works within a domain).
The key idea of multiple Share Scopes is: move shared dependency registration and resolution into different namespaces (Scopes), so you can isolate shared pools and layer policies.
Configuration Quick Map
The simplest way to understand multiple Share Scopes is to focus on what you configure on the producer, the consumer, and each shared entry. You don't need to learn runtime internal data structures or variable names.
- Producer: use shareScope to declare which Share Scopes this provider initializes (default:
default, supportsstring | string[]). - Consumer: use remotes[remote].shareScope to declare which Share Scopes the consumer aligns with a given provider (default:
default). - Shared entry: use
shared[pkg].shareScopeto decide which pool a dependency is registered / resolved in (see shared.shareScope).
What Happens for Different Combinations
When the consumer initializes a provider, it first aligns the Share Scopes based on both sides' shareScope settings — so the provider knows which shared pools to reuse — then the provider initializes shared dependencies according to its own shareScope.
To make the alignment and initialization relationship easier to describe, we use:
- HostShareScope for
remotes[remote].shareScopeconfigured on the consumer side - RemoteShareScope for
shareScopeconfigured on the provider side
Do not configure shareScope / remotes[remote].shareScope as ['default'] or []:
- Single Scope: use a string, not an array. The two follow different internal branches for share-pool alignment / initialization. If the consumer uses an array and the provider uses a string, the provider aligns Scopes using the consumer's list; if the provider uses an array, it only processes the provider's list.
- Empty array
[]: results in no Scopes being initialized (nodefault, no alignment) — this is a misconfiguration.
Share pools are provided by the consumer and initialized by the provider. Scopes the consumer does not list are filled in as {} (so missing scope names never crash); Scopes the provider does not list are not initialized. Both sides must list a Scope before a shared dep can really be reused under it.
Playground
Build Plugin Configuration
Producer
Key points:
shareScope: ['default','scope1']controls which Scopes the provider's remoteEntry initializes at runtime.shared[pkg].shareScopedecides which Scope a dependency registers / resolves under. If@company/design-systemis inscope1, it only participates in version selection and reuse within thescope1pool.
Consumer
Key points:
remotes[remote].shareScopecontrols which Scopes the consumer aligns when initializing a provider — they are passed to the provider asshareScopeKeys.- If the consumer is configured with multiple Scopes but the provider is single-Scope, the scopeMap is aligned but the provider only initializes sharing for its single Scope (see the combination table above). For multi-pool reuse to "really work", both consumer and provider usually need to agree on the same Scopes.
Pure Runtime (Runtime API)
If you do not declare providers and shared dependencies through the build plugin (for example, you want to register them dynamically at runtime), you can use the Runtime API for the same multi-Share-Scope effect. Two key APIs:
- Register providers:
registerRemotesorcreateInstance({ remotes }), declaring the Share Scopes to align viashareScope: string | string[]in each provider config. - Register shared dependencies:
registerSharedorcreateInstance({ shared }), deciding which Share Scope a dependency lands in viascope: string | string[]in each entry.
The field name when registering shared dependencies is scope, not shareScope (different from the build plugin's shared[pkg].shareScope).
Fine-grained Control with Runtime Hooks
Multiple Share Scopes essentially group shared pools by name. If you need finer control over Scope selection, alignment, and fallback strategies, you can use Runtime Hooks to intervene during the init phase or shared resolution.
1. Rewrite shareScopeKeys per provider (beforeInitContainer)
The example below forces legacy_remote to always use the legacy Scope (even if a different shareScope was set at build time or runtime registration):
2. Alias / fallback when a Scope is missing (initContainerShareScopeMap / resolveShare)
initContainerShareScopeMap: adjust each Scope'sshareScopemapping during the provider's share-pool initialization.resolveShare: override the final selection result by replacingargs.resolver. Returning{ ...args, scope: 'default' }alone is not enough in the current runtime implementation.
Example: if a package is not found in scope1, fall back to the default Scope:
You can also alias one Scope to another in initContainerShareScopeMap (so two Scopes share the same pool object):