State management
This article dives into the topic of state management and how to apply this in an Angular application.
What is state
State, in this context, refers to all of the values retained by an application at any given time, with respect to previous interactions with the system. This state can be changed by executing methods/functions.
This state can be anything: a boolean that is set to true to display something in your component, or an entire list of entities that you retrieve from your backend.
An application, for example, can retrieve a list of favorited movies for a given user (method) that mutates the state, updating the user’s list of movies. These movies are then displayed by the application in two places (that both receive the latest version of the movies list when they subscribe to the state changes).
State management patterns
There are several ways to work with state in your application. There are no real wrongs or rights, but its a good idea to start simple and scale up as your application’s complexity grows.
Observer pattern
Out of the box, Angular adopts the observer pattern by using RxJS subjects. Read more about working with RxJS here. Using this reactive pattern a viable way for managing state in Angular.
In Angular, a simple service could suffice as state manager.
Signals
The Angular team is working on implementing signals into the framework, and are currently in developer preview in Angular 16. When the time comes this article will include information on how to add reactivity to your applications using signals.
Flux / store pattern
More complex applications, especially with lots of inter-component communication, can benefit from an architecture with a global, single source of truth for your shared state.
In this pattern, the state is centrally managed in a ‘store’ – a centralized object containing all the data that is shared across the application. This state can only be mutated by a predefined set of actions, and then to process these actions in a distinct sequence.
This leads to two main aspects of state, namely:
-
State is immutable
You don’t update a single value in your state tree, but you emit a new reference to the the mutated state for each action that mutates the state.
-
Unidirectional (one-way) data flow
The view layer will only dispatch actions. These actions are handled by the store that mutates the state and emits a new version of the state. Any subscriber to that (piece of) state will receive an updated version to be handled accordingly.
Example libraries implementing this pattern: Elf, Redux, NgRx and NGXS.
Follow the facade pattern
There are many different state management tools available, and different patterns you can follow. Most applications will adopt more than one pattern, depending on the application or use case.
Also, more complex applications might rely on external (sometimes third-party) services that might change their public API, potentially breaking your application’s implementation for that service. Or, at some point you want to swap the state management library you are using for another.
In any of these cases, your application will benefit from using the facade pattern.
A facade is a layer of abstraction between the place where information can be retrieved (i.e. any state slice, custom observables from an Angular service, etc.) and your consumers (i.e. components, services, directives etc.). It expose a uniform API for your application. With this pattern, state management logic (selection, mutations) is hidden from your consumers, making it easy for developers to use your state, swap your state solution(s), or use different solutions for specific features.
Example
In this example, we build a facade wrapper around the following Angular service:
movie.store.ts:
@Injectable({
providedIn: 'root',
})
export class MovieStore {
readonly #httpClient = inject(HttpClient);
readonly #moviesMapSubject = new BehaviorSubject<Map<string, Movie>>(new Map());
readonly #moviesMap$ = this.moviesMapSubject.asObservable();
readonly movies$ = this.#moviesMap$.pipe(map((movieMap) => Array.from(movieMap.values())));
readonly movieById$ = (id: string) => this.#moviesMapSubject.pipe(map((movies) => movies.get(id)));
fetchAll() {
return this.#httpClient.get('/api/movie').pipe(
tap((movies) => {
const map = new Map<string, Movie>();
movies.forEach((movie) => map.set(movie.id, movie));
this.#emitState(map);
}),
);
}
#emitState(map: Map<string, Movie>) {
this.#moviesMapSubject.next(map);
}
}
This information could also come from another source, such as a state slice from a state management library or another (external) service. No matter what, we want to create an abstraction layer to expose a uniform public API for our consumers:
movie.facade.ts:
@Injectable({
providedIn: 'root',
})
export class MovieFacade {
readonly #movieStore = inject(MovieStore);
readonly movies$ = this.#movieStore.movies$;
readonly movieById$ = this.#movieStore.movieById$;
public fetchMovies() {
return this.#movieStore.fetchAll();
}
}
This facade hides all implementation details about the state from your consumers, making it super easy to swap the place you host your state in the future. As long as your public API does not change, your application’s implementation can remain the same.
With this facade, we can now access our state super easily in your components. The component below shows a list of clickable movie titles, and routes to a single item using the Angular router params:
movies.component.ts:
@Component({
selector: 'app-movies',
standalone: true,
imports: [CommonModule, RouterModule],
templateUrl: './start.component.html',
styleUrls: ['./start.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StartComponent {
readonly #moviesFacade = inject(MovieFacade);
readonly movies$ = this.#moviesFacade.movies$;
readonly selectedMovie$ = getRouteParam('id').pipe(mergeMap((id) => (id ? this.#moviesFacade.movieById$(id) : of(undefined))));
constructor() {
this.#moviesFacade.fetchMovies().subscribe();
}
}
movies.component.html:
<ul *ngIf="movies$ | async as movies">
<li *ngFor="let movie of movies">
<a [routerLink]="'/start/' + movie.id">{{ movie.title }}</a>
</li>
</ul>
<section *ngIf="selectedMovie$ | async as selectedMovie; else empty">
<h1>{{ selectedMovie.title }}</h1>
</section>
<ng-template #empty>
<p>Please select a movie.</p>
</ng-template>
Since this component uses the async pipe, there is no need for manual subscriptions on the observables exposed by the facade.
Consider your state scope
State can live anywhere in your application.
Use component state
Sometimes (many times, actually) state is limited to a component, or tree of components. In those cases, you can opt to use a normal Angular service, and provide it in your component’s providers array. This will make sure each component gets a new instance of the service.
To do this, set the providedIn property of your service decorator to any:
slide.service.ts:
@Injectable({
providedIn: 'any', // <-- set providedIn to any
})
export class SlideService {
public readonly id$ = new BehaviorSubject('');
public readonly title$ = new BehaviorSubject('');
constructor() {
this.id$.next((Math.random() + 1).toString(36).substring(7));
}
public updateTitle(title: string) {
this.title$.next(title);
}
}
and add the service to your component’s providers array:
slide.component.ts:
@Component({
selector: 'app-slide',
standalone: true,
providers: [SlideService], // <-- provide the service in your component
imports: [CommonModule],
templateUrl: './slide.component.html',
styleUrls: ['./slide.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SlideComponent {
readonly #slideService = inject(SlideService);
readonly title$ = this.#slideService.title$;
readonly id$ = this.#slideService.id$;
@HostListener('click')
public updateTitle() {
this.slideService.updateTitle(`
This is the new title.
It will only be applied to
the service instance for
this particalar slide.
`);
}
}
NgRx: use feature or component state
When using NgRx, you can create slices of state for each feature using feature creators. Or, to scope to your component only, you can opt to use the NgRx component store.
Working with entities
If you have a list of entities with a unique identifier, create a dictionary copy of your data for quick lookup by identifier. You can refer to the demo project in this repository for an example.
If you are using NgRx, you can use the NgRx Entity plugin.
Using a library
Using libraries can certainly help keeping your code organized, but can also add unnecessary boilerplate and complexity to your application. In general, the bigger your application (number of components, use cases, features etc.), the more your project can benefit from using a centralized state management library.
Most libraries will implement a flux-like pattern, adding significant amounts of boilerplate and complexity.