DEV Community

Simone Boccato
Simone Boccato

Posted on

Angular - NgRx Facade Design Pattern

In this article I'll show how I use the Facade Design Pattern on my angular projects.

Facade Design Pattern

I read about this pattern from this website: facade
This is a structural design pattern that provides a simplified interface to a library, a framework, or any other complex set of classes.

The goal of the Facade is to provide a simple interface to complex subsystem which contain lots parts.

How to implement it

In my angular project’s signals-first architecture, the Facade pattern serves as a critical architectural layer between components and the underlying state management (NgRx Signal Store).

1. Define the State

First, we define the shape of our state and its initial values. Using a _ prefix for internal state properties is a common convention when we plan to rename them in the public API.

// auth.state.ts
export type LocaleModel = 'en' | 'it';
export type UserData = {
  email: string;
  firstName: string;
  lastName: string;
  name: string;
  local: LocaleModel;
  phone?: string;
  phonePrefix?: string;
  group: 'FREE' | 'PROFESSIONAL' | 'ENTERPRISE';
};

export type AuthState = {
  _locale: LocaleModel | null;
  _loggedUser: UserData | null;
};

export const initialAuthState: AuthState = {
  _locale: null,
  _loggedUser: null,
} as const;
Enter fullscreen mode Exit fullscreen mode

2. Create the Signal Store

The store handles the "how" of state management. It manages private state and exposes derived state via computed signals.

import { computed } from '@angular/core';
import { signalStore, withComputed, withMethods, withState } from '@ngrx/signals';

export const AuthStore = signalStore(
  { providedIn: 'root' },
  withState(initialAuthState),
  withComputed((store) => {
    const $isFreeUser = computed(() => store._loggedUser()?.group === 'FREE');
    const $isProfessionalUser = computed(() => store._loggedUser()?.group === 'PROFESSIONAL');
    const $isEnterpriseUser = computed(() => store._loggedUser()?.group === 'ENTERPRISE');

    return {
      $isFreeUser,
      $isProfessionalUser,
      $isEnterpriseUser,
    };
  }),
  withMethods((store) => {
    const initialize = () => {};
    const changeSelectedLanguage = () => {};
    const setLoggedUser = () => {};
    const updateUser = () => {};
    const logout = () => {};

    return {
      initialize,
      changeSelectedLanguage,
      setLoggedUser,
      updateUser,
      logout,
    };
  }),
);

export type AuthStore = InstanceType<typeof AuthStore>;
Enter fullscreen mode Exit fullscreen mode

3. Implement the Facade

The Facade provides the "what" to the components. We use the OmitSymbols utility to ensure the Facade strictly implements the functional API of the Store while automatically hiding internal NgRx Signal Store symbols that shouldn't be exposed to components.

// auth.facade.ts
import { inject, Injectable, type Signal } from '@angular/core';

/**
 * Drops every symbol-keyed property *plus* any keys in `U`.
 */
export type OmitSymbols<T, U extends PropertyKey = never> = {
  [K in keyof T as K extends symbol ? never : K extends U ? never : K]: T[K];
};

@Injectable({ providedIn: 'root' })
export class AuthFacade implements OmitSymbols<AuthStore> {
  readonly #authStore = inject(AuthStore);

  // Expose signals with $ prefix
  readonly $locale = this.#authStore._locale;
  readonly $loggedUser = this.#authStore._loggedUser;

  readonly $isFreeUser = this.#authStore.$isFreeUser;
  readonly $isProfessionalUser = this.#authStore.$isProfessionalUser;
  readonly $isEnterpriseUser = this.#authStore.$isEnterpriseUser;

  initialize() {
    this.#authStore.initialize();
  }
  changeSelectedLanguage() {
    this.#authStore.changeSelectedLanguage();
  }
  setLoggedUser() {
    this.#authStore.setLoggedUser();
  }
  updateUser() {
    this.#authStore.updateUser();
  }
  logout() {
    this.#authStore.logout();
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Use inside Angular Components

Components consume a clean, reactive API. They are decoupled from the store's implementation details.

  @Component({...})
  export class HeaderComponent {
    readonly #auth = inject(AuthFacade);

    // Direct access to signals from facade
    $user = this.#auth.$loggedUser; 
    $isFreeUser = this.#auth.$isFreeUser;

    onLogout() {
     this.#auth.logout();
    }
   }
Enter fullscreen mode Exit fullscreen mode

Why this matters in real projects

Based on the AuthFacade pattern, here is why this is effective:

Abstraction of Complexity

Components shouldn't care how state is managed. The Facade hides the complexity of the AuthStore. If you decide to move a piece of state from a global Store to a specialized Service or even combine it with Route Params, you only update the Facade. Your components remain untouched.

Simplified Public API & Integrity

A Store often contains internal state and complex update logic. The Facade acts as a gatekeeper:

  • It exposes only what the component needs.
  • It protects the store's integrity by not exposing patchState or internal state directly.
  • It provides a semantic interface: this.#authFacade.logout() is clearer than interacting with store internals.

Simplified Testing

One of the biggest benefits is simplified unit testing. You can easily mock the AuthFacade in component tests without setting up a full NgRx Signal Store:

   { provide: AuthFacade, useValue: { $loggedUser: signal(mockUser), logout: vi.fn() } }
Enter fullscreen mode Exit fullscreen mode

Enforcement of Naming Conventions

The Facade reinforces the project's reactive patterns. Consistently exposing signals with the $ prefix gives developers an immediate visual cue that they are working with a Signal, promoting a "Signals-First" mindset.

Orchestration & Derived State

Facades are perfect for orchestrating multiple sources, and they can combine data from a Signal Store, a Standard Service, and Route Params into a single computed signal, providing the component with exactly one object to bind to.

Top comments (0)