Understanding NgRx Signal Store
The NgRx Signal Store provides Angular developers with a state management approach built directly on the reactive signals feature introduced in Angular 16. Compared to the conventional NgRx Store, which depends heavily on observables and dispatched actions, the Signal Store offers a more streamlined and efficient alternative. It brings the power of Angular signals into the NgRx ecosystem, enabling state management that feels both intuitive and reactive while remaining easier to reason about than its predecessor.
Additional resources on signals from angular.love:
- https://angular.love/en/angular-signals-a-new-feature-in-angular-16
Further reading on the Signal Store from angular.love:
- https://angular.love/en/breakthrough-in-state-management-discover-the-simplicity-of-signal-store-part-1
Exploring Hexagonal Architecture
Hexagonal architecture, also referred to as the Ports and Adapters pattern, keeps the core business logic decoupled from external concerns. This is achieved by defining interaction points through interfaces—known as ports—and delegating the concrete implementations to adapters. The result is improved modularity, enhanced testability, and greater flexibility, since the core remains untouched by infrastructure changes, which simplifies both maintenance and evolution of the application.
When the Hexagon Pattern is applied within Angular and combined with the framework's Dependency Injection (DI), it becomes an elegant solution for handling complex interactions. This combination lets developers hide intricate details behind a clean, simplified API that the rest of the application consumes, which strengthens modularity, maintainability, and testability.
Learn more about dependency injection from angular.love:
https://angular.love/en/dependency-injection-in-angular-everything-you-need-to-know For additional insights on Hexagon Architecture, see:
- https://angular.love/ports-and-adapters-vs-hexagonal-architecture-is-it-the-same-pattern
Harnessing Type Inference in Signal Store
NgRx SignalStore takes full advantage of TypeScript’s type inference to deliver a highly type-safe state management experience without demanding explicit type declarations. This built-in inference cuts down on boilerplate, makes the code more readable, and enforces type safety throughout the application. As a result, maintaining and refactoring becomes less error-prone, and runtime issues stemming from type mismatches are largely prevented. Developers get an API that feels intuitive, supports contemporary Angular practices, and requires minimal effort while maximizing reliability.
A Minimal Port and Adapter Approach for NgRx Signal Store
Pairing NgRx SignalStore with the Hexagonal Architecture brings a robust strategy for state management that still retains all the type-inference advantages TypeScript has to offer. This integration supports the modularity and adaptability of the hexagon pattern, drawing distinct lines between business rules and infrastructure, all without piling on extra code or complexity. Developers gain the testing and maintainability perks of hexagonal design while enjoying the straightforward, focused nature of NgRx SignalStore.
In TypeScript, adapters are typically built on interface implementation. Here, however, we turn to the 'satisfies' operator instead:
import { Type } from "@angular/core";
export interface Fruit {
id: string;
name: string;
}
export interface FruitService {
fruits: Signal<Fruit[]>;
loadFruits(): void;
}
const FruitServiceAdapter = signalStore(
withState({ fruits: [] as Fruit[] }),
withMethods((store) => {
return {
loadFruits: async () => {
const fruits = await fetch('https://api.example.com/fruits').then(
(res) => res.json()
);
patchState(store, {
fruits,
});
},
};
})
) satisfies Type<FruitService>;
The 'Satisfies' operator makes sure the value aligns with the given type, yet it avoids changing the actual type of that value (such as the one derived from type inference). In this way, the inferred type for the signal store remains intact, while we still confirm that the port has been correctly implemented.
Next, we can set up handy utilities to inject the adapter behind the port:
const fruitServiceInjectionToken = new InjectionToken<FruitsService>(
'fruits-service'
);
export function provideFruitService(): Provider {
return {
provide: fruitServiceInjectionToken,
useClass: FruitServiceAdapter,
};
}
export function injectFruitService(): FruitsService {
return inject(fruitServiceInjectionToken);
}
Here is how this looks when used inside a component or service:
@Component({
selector: 'app-root',
standalone: true,
template: ``,
providers: [provideFruitService()],
})
export class App {
private fruitService = injectFruitService();
constructor() {
this.fruitService.loadFruits();
}
}
Explore a live example on Stackblitz:
https://stackblitz.com/edit/stackblitz-starters-pbmwtt?file=src%2Ffruit.service.ts
Alternatively, abstract classes can streamline the injection utilities, as covered in the angular.love discussion on Ports and Adapters:
- https://angular.love/ports-and-adapters-vs-hexagonal-architecture-is-it-the-same-pattern
Wrapping Up
Bringing NgRx SignalStore and Hexagonal Architecture together gives Angular teams a modern, type-safe path to state management. By leaning on TypeScript’s inference capabilities, the usual simplicity of SignalStore is preserved, while the modular and testable structure of the hexagon pattern is introduced. This blend reduces boilerplate, boosts code clarity, and promotes a well-organized architecture with a clear separation of concerns. With just a small amount of extra code, developers can put in place a state management solution that is clean, maintainable, and ready to scale—one that reflects both the reactive programming model and the tenets of clean architecture. This combination empowers developers to handle complex applications with greater ease and confidence.
