DEV Community

Cover image for How to Use NgRx Signals for State Management in Angular Micro-Frontend Apps
mhmoud ashour
mhmoud ashour

Posted on

How to Use NgRx Signals for State Management in Angular Micro-Frontend Apps

Angular state management has come a long way.

From services with BehaviorSubjects, to NgRx Store, to now — NgRx Signals.

If you’re building a Micro-Frontend architecture in 2026, NgRx Signals is the state management approach you should be using. Here’s why — and exactly how to implement it.


What Are NgRx Signals?

NgRx Signals is a reactive state management library built on top of Angular’s native Signals API. It gives you:

  • A simple, lightweight store
  • No boilerplate compared to classic NgRx
  • Fine-grained reactivity
  • Works perfectly across micro-apps

The Problem With State in Micro-Frontends

In a Micro-Frontend setup, each app is independent:

Shell App (Port 4200)
├── Auth App (Port 4201)
└── Dashboard App (Port 4202)
Enter fullscreen mode Exit fullscreen mode

The challenge — how do you share state between them without tight coupling?

Classic NgRx is overkill for this. BehaviorSubjects get messy. NgRx Signals hits the sweet spot.


Setting Up NgRx Signals

Install the package:

npm install @ngrx/signals
Enter fullscreen mode Exit fullscreen mode

Step 1 — Create a Shared Signal Store

In your Shell app, create a global store:

// shell/src/app/store/app.store.ts
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';

export interface AppState {
  theme: 'light' | 'dark';
  language: 'en' | 'ar' | 'fr';
  isAuthenticated: boolean;
  user: { name: string; role: string } | null;
}

const initialState: AppState = {
  theme: 'light',
  language: 'en',
  isAuthenticated: false,
  user: null,
};

export const AppStore = signalStore(
  { providedIn: 'root' },
  withState(initialState),
  withMethods((store) => ({
    setTheme(theme: 'light' | 'dark') {
      patchState(store, { theme });
    },
    setLanguage(language: 'en' | 'ar' | 'fr') {
      patchState(store, { language });
    },
    setUser(user: { name: string; role: string }) {
      patchState(store, { user, isAuthenticated: true });
    },
    logout() {
      patchState(store, { user: null, isAuthenticated: false });
    },
  }))
);
Enter fullscreen mode Exit fullscreen mode

Step 2 — Use the Store in Your Shell Component

// shell/src/app/app.component.ts
import { Component, inject, effect } from '@angular/core';
import { AppStore } from './store/app.store';

@Component({
  selector: 'app-root',
  template: `
    <div [class]="store.theme() === 'dark' ? 'dark-theme' : 'light-theme'"
         [dir]="store.language() === 'ar' ? 'rtl' : 'ltr'">
      <router-outlet />
    </div>
  `
})
export class AppComponent {
  store = inject(AppStore);

  constructor() {
    // Persist theme to localStorage
    effect(() => {
      localStorage.setItem('theme', this.store.theme());
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3 — Access Store in Micro-Apps

Since the store is providedIn: 'root' and shared via the Shell, micro-apps can inject it directly:

// dashboard/src/app/dashboard.component.ts
import { Component, inject } from '@angular/core';
import { AppStore } from '../../../shell/src/app/store/app.store';

@Component({
  selector: 'app-dashboard',
  template: `
    <div>
      <h1>Welcome, {{ store.user()?.name }}</h1>
      <button (click)="toggleTheme()">
        Switch to {{ store.theme() === 'light' ? 'Dark' : 'Light' }} mode
      </button>
    </div>
  `
})
export class DashboardComponent {
  store = inject(AppStore);

  toggleTheme() {
    const newTheme = this.store.theme() === 'light' ? 'dark' : 'light';
    this.store.setTheme(newTheme);
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4 — Add State Persistence

Keep state alive across page refreshes:

// shell/src/app/store/app.store.ts
import { signalStore, withState, withMethods, withHooks, patchState } from '@ngrx/signals';

export const AppStore = signalStore(
  { providedIn: 'root' },
  withState<AppState>(() => ({
    theme: (localStorage.getItem('theme') as 'light' | 'dark') || 'light',
    language: (localStorage.getItem('language') as 'en' | 'ar' | 'fr') || 'en',
    isAuthenticated: !!localStorage.getItem('auth_token'),
    user: null,
  })),
  withMethods((store) => ({
    setTheme(theme: 'light' | 'dark') {
      localStorage.setItem('theme', theme);
      patchState(store, { theme });
    },
    setLanguage(language: 'en' | 'ar' | 'fr') {
      localStorage.setItem('language', language);
      patchState(store, { language });
    },
    setUser(user: { name: string; role: string }) {
      patchState(store, { user, isAuthenticated: true });
    },
    logout() {
      localStorage.removeItem('auth_token');
      patchState(store, { user: null, isAuthenticated: false });
    },
  }))
);
Enter fullscreen mode Exit fullscreen mode

Step 5 — Feature Store for Each Micro-App

Each micro-app can also have its own local store:

// dashboard/src/app/store/dashboard.store.ts
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';

interface DashboardState {
  widgets: Widget[];
  isLoading: boolean;
  selectedWidget: Widget | null;
}

export const DashboardStore = signalStore(
  withState<DashboardState>({
    widgets: [],
    isLoading: false,
    selectedWidget: null,
  }),
  withMethods((store) => ({
    setLoading(isLoading: boolean) {
      patchState(store, { isLoading });
    },
    setWidgets(widgets: Widget[]) {
      patchState(store, { widgets, isLoading: false });
    },
    selectWidget(widget: Widget) {
      patchState(store, { selectedWidget: widget });
    },
  }))
);
Enter fullscreen mode Exit fullscreen mode

NgRx Signals vs Classic NgRx

Feature Classic NgRx NgRx Signals
Boilerplate Actions, Reducers, Effects, Selectors Just a store
Learning curve High Low
Bundle size Larger Smaller
Reactivity Observable-based Signal-based
Angular 17+ fit Good Excellent
Micro-Frontend use Complex Simple

Why NgRx Signals is Perfect for Micro-Frontends

  • No boilerplate — less code to share between apps
  • Fine-grained updates — only components using changed signals re-render
  • Simple injection — just inject(AppStore) anywhere
  • Persistent state — easy localStorage integration
  • TypeScript friendly — full type safety out of the box

Want This Already Set Up?

I built NgMFE Starter Kit — a complete Angular 21 Micro-Frontend boilerplate with NgRx Signals pre-configured:

  • ⚡ Angular 21 + Native Federation
  • 🔐 JWT Auth + Route Guards
  • 🌍 Arabic RTL support
  • 🌙 Dark/Light mode with NgRx Signals
  • 🚀 CI/CD with GitHub Actions + Vercel
  • ✅ 13 unit tests passing

🔴 Live Demo: https://ng-mfe-shell.vercel.app
(login: admin/admin)

🛒 Get the kit:
👉 https://mhmoudashour.gumroad.com/l/hdditr

💼 Need it set up for your project?
👉 https://www.upwork.com/services/product/development-it-set-up-angular-micro-frontend-architecture-for-your-enterprise-app-2037100004401414520?ref=project_share


Have questions about NgRx Signals or state management in Micro-Frontends? Drop them in the comments! 🙏

Top comments (0)