Sharing State & Dependencies
Independently deployed micro frontends each ship their own bundle, yet they run together in a single browser tab — sharing one DOM, one set of globals, and ideally one copy of Angular. Getting sharing right is the difference between a fast, coherent app and a bloated one that downloads Angular five times and crashes when two remotes disagree on a library version. This page covers how to share singleton dependencies, how to survive version mismatches, and how to communicate state across apps that deploy on their own schedules.
Why sharing matters
Every micro frontend that bundles Angular adds ~150 KB+ of framework code. If the shell and three remotes each ship their own copy, users download Angular four times and the browser instantiates four independent framework runtimes. Beyond bandwidth, Angular relies on singletons: a single NgZone, a single set of injectable platform providers, and a single reflection/metadata registry. Loading two live copies of @angular/core in the same tab leads to subtle, hard-to-debug failures — change detection that never fires, inject() throwing “outside injection context”, or RxJS instanceof checks failing across copies.
The goal is therefore to share framework and library code as singletons while keeping application state explicit and loosely coupled.
Sharing singleton dependencies
Both Module Federation and Native Federation let you declare shared packages so the first micro frontend to load a dependency wins and the rest reuse it. Mark anything that must be a singleton — Angular packages, RxJS, and any shared design-system library — accordingly.
// federation.config.js (Native Federation)
const { withNativeFederation, shareAll } = require('@angular-architects/native-federation/config');
module.exports = withNativeFederation({
name: 'shell',
shared: {
...shareAll({
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
}),
},
skip: ['rxjs/ajax', 'rxjs/fetch', 'rxjs/webSocket'],
});
shareAll reads your package.json and shares every production dependency. The three flags do the heavy lifting:
| Option | Meaning | Recommended for framework |
|---|---|---|
singleton: true | Only one version may be live at runtime | Yes — required for @angular/*, rxjs |
strictVersion: true | Throw on an incompatible version instead of silently loading a second copy | Yes — surfaces drift early |
requiredVersion: 'auto' | Derive the semver range from package.json | Yes |
With Module Federation the shape is the same, declared in shared inside the ModuleFederationPlugin config; singleton, strictVersion, and requiredVersion mean exactly the same thing.
Always share
@angular/core,@angular/common,@angular/router, andrxjsas singletons. Sharing the router as a singleton is what lets a remote register routes that the shell’sRouteractually sees.
Handling version mismatches
Independently deployed apps drift. The shell might run Angular 18.2 while a slowly-updated remote still ships 18.0. How federation reacts depends on your flags:
singleton: true,strictVersion: false— the highest compatible version loads; lower ones reuse it and a warning is logged. This is the pragmatic default and tolerates patch/minor drift.singleton: true,strictVersion: true— an incompatible range throws at load time. Safer, but a careless major bump in one remote can break the whole shell.- Major-version mismatch (e.g. shell on 18, remote on 17) — singleton sharing cannot reconcile this; one app will fail to bootstrap. Coordinate major upgrades across teams, or temporarily allow the lagging remote to bundle its own Angular until it catches up.
Output: a typical strict-version conflict in the console:
Error: Shared module @angular/core doesn't satisfy the required version 18.2.0 (singleton).
Available version: 18.0.4
A practical policy: keep singleton: true for everything, use strictVersion: false so minor drift degrades gracefully, and gate major Angular upgrades behind a coordinated release where every team bumps together.
Communicating state across micro frontends
Shared code is a singleton; shared state should not be a hidden global. The cleanest pattern is to publish a tiny, framework-agnostic contract that every micro frontend depends on, and exchange data through events or signals rather than reaching into each other’s internals.
Browser-native events
The most decoupled approach uses CustomEvent on window. No shared library is needed, so a React or vanilla micro frontend could participate too.
import { Injectable } from '@angular/core';
import { Observable, fromEvent, map } from 'rxjs';
export interface AuthChange {
userId: string | null;
}
@Injectable({ providedIn: 'root' })
export class CrossAppBus {
emit(type: string, detail: unknown): void {
window.dispatchEvent(new CustomEvent(type, { detail }));
}
on<T>(type: string): Observable<T> {
return fromEvent<CustomEvent<T>>(window, type).pipe(map((e) => e.detail));
}
}
A remote subscribes with signals so templates stay reactive:
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { CrossAppBus, AuthChange } from 'shared-contracts';
@Component({
selector: 'app-orders',
standalone: true,
template: `
@if (auth()?.userId; as id) {
<p>Orders for {{ id }}</p>
} @else {
<p>Please sign in.</p>
}
`,
})
export class OrdersComponent {
private bus = inject(CrossAppBus);
auth = toSignal(this.bus.on<AuthChange>('auth:change'));
}
The shell broadcasts on login: bus.emit('auth:change', { userId: '42' }).
A shared singleton store
When you need shared reactive state rather than fire-and-forget events, expose a signal-based store from a library that is itself declared as a singleton. Because federation guarantees one copy, every app reads and writes the same signal instance.
import { Injectable, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class SessionStore {
private _userId = signal<string | null>(null);
readonly userId = this._userId.asReadonly();
readonly isAuthenticated = computed(() => this._userId() !== null);
signIn(id: string) { this._userId.set(id); }
signOut() { this._userId.set(null); }
}
Keep the shared contract tiny and stable — IDs, tokens, and event shapes only. Sharing rich domain models couples teams and forces lockstep deploys, defeating the point of micro frontends.
Best practices
- Share
@angular/*andrxjsassingleton: trueto avoid duplicate framework runtimes and broken change detection. - Prefer
strictVersion: falsefor everyday minor drift, but coordinate major Angular upgrades across all teams in one release. - Publish a small, versioned
shared-contractslibrary for event types and store interfaces — never let apps reach into each other’s components. - Communicate volatile, fire-and-forget data via
CustomEvent; use a singleton signal store only for genuinely shared reactive state. - Wrap RxJS streams in
toSignal()so cross-app updates flow naturally into modern templates andOnPushcomponents. - Monitor bundle output to confirm shared packages appear once; a duplicated
@angular/corechunk is a red flag that sharing is misconfigured. - Treat shared state as a public API: version it, document it, and avoid breaking changes that would force every remote to redeploy.