Skip to content
Angular ng microfrontends 5 min read

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:

OptionMeaningRecommended for framework
singleton: trueOnly one version may be live at runtimeYes — required for @angular/*, rxjs
strictVersion: trueThrow on an incompatible version instead of silently loading a second copyYes — surfaces drift early
requiredVersion: 'auto'Derive the semver range from package.jsonYes

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, and rxjs as singletons. Sharing the router as a singleton is what lets a remote register routes that the shell’s Router actually 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/* and rxjs as singleton: true to avoid duplicate framework runtimes and broken change detection.
  • Prefer strictVersion: false for everyday minor drift, but coordinate major Angular upgrades across all teams in one release.
  • Publish a small, versioned shared-contracts library 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 and OnPush components.
  • Monitor bundle output to confirm shared packages appear once; a duplicated @angular/core chunk 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.
Last updated June 14, 2026
Was this helpful?