Image for post: Angular app state management: the final thought

Angular app state management: the final thought

I have recently worked a lot on a single-page application. Even small apps might need a state manager when it grows and you are adding more features. Data will become hard to manage and the state of the app is not so clear if components have to use only custom events, and input \ output parameters. Also, a child component can have sub-child components that can have problems sharing data and sending them to other components.
One important aspect of building web applications is managing the state of the application, which refers to the data that is displayed and manipulated by the user. In Angular, there are several options for managing state, and in this blog post, we will explore some of the most common approaches.

One way to manage state in Angular is through the use of services. Services are a way to share data and functionality between different components in an Angular application. They can be used to store and retrieve data from a server, or to perform complex calculations and logic. Services are a good choice for state management because they are easy to test and maintain, and they can be injected into any component that needs them.

Another way to manage state in Angular is through the use of observables. Observables are a way to asynchronously stream data to components, and they can be used to manage state in a reactive manner. Observables allow components to subscribe to data changes, and they are a good choice for managing state in Angular because they are efficient and scalable.

Finally, Angular also provides a state management library called NgRx, which is based on the Redux pattern. NgRx is a powerful tool for managing state in Angular applications, and it is particularly useful for applications with a large amount of complex data. NgRx uses a single store for all application state, and it provides a set of tools for managing state changes in a predictable and consistent way.

In summary, there are several options for managing state in Angular, including services, observables, and NgRx. The right choice will depend on the needs of your application, and it is important to choose a solution that is efficient, scalable, and easy to maintain.

What should stay in a state and what should stay in a component?

This is a frequent question I asked myself because we can have variables on components and variables in our state management. I think it's better to move data in a state manager only when they have to be shared with other components.
All variables for the user interface must stay in the components.

State management using shared services, Subject and BehaviorSubject

It is possible to build a static state manager using Subject and BehaviourSubject. What do I mean by "static"?  NgRx and the Redux pattern use a Store to keep data even when the browser is refreshed manually. Usually, a single-page application or even apps of medium size doesn't need to keep data in a store or work offline for some reasons like a progressive web.
Using Subject or even better the BehaviorSubject in many cases, help the developer to even manage subscription in services, and keep all Angular components free of all subscription. Actually, we want components to contain only elements we need for our UI.
How all of this is possible? I have made a lot of research finding what is the possible solution to obtain this architecture and code organization until I found this mind-blowing example on Stackblitz. Let's see the main state service code:

import { BehaviorSubject, Observable } from 'rxjs';import { distinctUntilChanged, map } from 'rxjs/operators';export class StateService<T> { private state$: BehaviorSubject<T>; protected get state(): T { return this.state$.getValue(); } constructor(initialState: T) { this.state$ = new BehaviorSubject<T>(initialState); } protected select<K>(mapFn: (state: T) => K): Observable<K> { return this.state$.asObservable().pipe( map((state: T) => mapFn(state)), distinctUntilChanged() ); } protected setState(newState: Partial<T>) { this.state$.next({ ...this.state, ...newState, }); }}

And this is a service that extends the state service. The example keeps the state services separated from the services that can create API requests or other things we need for similar purposes. The state service needs only to manage the data we want to retrieve among multiple modules and components:

import { Injectable } from '@angular/core';import { StateService } from '../../../shared/state.service';import { Todo } from '../models/todo';import { Filter } from '../models/filter';import { Observable } from 'rxjs';import { map, shareReplay } from 'rxjs/operators';import { TodosApiService } from './api/todos-api.service';interface TodoState { todos: Todo[]; selectedTodoId: number; filter: Filter;}const initialState: TodoState = { todos: [], selectedTodoId: undefined, filter: { search: '', category: { isBusiness: false, isPrivate: false, }, },};@Injectable({ providedIn: 'root',})export class TodosStateService extends StateService<TodoState> { private todosFiltered$: Observable<Todo[]> = this.select((state) => { return getTodosFiltered(state.todos, state.filter); }); todosDone$: Observable<Todo[]> = this.todosFiltered$.pipe( map((todos) => todos.filter((todo) => todo.isDone)) ); todosNotDone$: Observable<Todo[]> = this.todosFiltered$.pipe( map((todos) => todos.filter((todo) => !todo.isDone)) ); filter$: Observable<Filter> = this.select((state) => state.filter); selectedTodo$: Observable<Todo> = this.select((state) => { if (state.selectedTodoId === 0) { return new Todo(); } return state.todos.find((item) => item.id === state.selectedTodoId); }).pipe( // Multicast to prevent multiple executions due to multiple subscribers shareReplay({ refCount: true, bufferSize: 1 }) ); constructor(private apiService: TodosApiService) { super(initialState); this.load(); } selectTodo(todo: Todo) { this.setState({ selectedTodoId: todo.id }); } initNewTodo() { this.setState({ selectedTodoId: 0 }); } clearSelectedTodo() { this.setState({ selectedTodoId: undefined }); } updateFilter(filter: Filter) { this.setState({ filter: { ...this.state.filter, ...filter, }, }); } // API CALLS load() { this.apiService.getTodos().subscribe((todos) => this.setState({ todos })); } create(todo: Todo) { this.apiService.createTodo(todo).subscribe((newTodo) => { this.setState({ todos: [...this.state.todos, newTodo], selectedTodoId: newTodo.id, }); }); } update(todo: Todo) { this.apiService.updateTodo(todo).subscribe((updatedTodo) => { this.setState({ todos: this.state.todos.map((item) => (item.id === todo.id ? updatedTodo : item)), }); }); } delete(todo: Todo) { this.apiService.deleteTodo(todo).subscribe(() => { this.setState({ selectedTodoId: undefined, todos: this.state.todos.filter((item) => item.id !== todo.id), }); }); }}function getTodosFiltered(todos, filter): Todo[] { return todos.filter((item) => { return ( item.title.toUpperCase().indexOf(filter.search.toUpperCase()) > -1 && (filter.category.isBusiness ? item.isBusiness : true) && (filter.category.isPrivate ? item.isPrivate : true) ); });}

Of course, the code can be improved but I really liked this structure and ideas. I think they can be used in real-world projects without problems.

NgRx

NGRX is a library based on the redux pattern, which is a popular way to manage states in JavaScript applications.
It provides a set of utilities for managing state in Angular applications, including:

  • Store: a centralized place to store the state of the application. The store is observable, which means you can subscribe to it and receive updates when the state changes.

  • Actions: used to describe changes to the state of the application. You can dispatch actions to the store, which will cause the store to update its state.

  • Reducers: Reducers are pure functions that take in the current state of the application and action, and return a new state based on the action.

  • Selectors: functions that you can use to extract specific pieces of data from the store. You can use selectors to avoid having to manage state directly in your components, which can make your code easier to test and maintain.

NgRx also provides tools for debugging and testing, such as the ability to replay actions, and the ability to create mock stores for testing.
Overall, it is a powerful tool for managing state in Angular applications and can help you create scalable and maintainable applications.

Other state manager third part libraries for Angular projects

There are many different options for managing state in Angular applications, and the best choice for your project will depend on your specific needs and preferences. Here we have some other libraries:

  • Redux: This is a popular JavaScript library for managing states that are often used with Angular. It provides a set of tools for managing the state in a predictable and consistent way, including a store, actions, and reducers.

  • Ngxs: a state management library for Angular that is inspired by the redux pattern. It provides a set of tools for managing the state, including a store, actions, and selectors.

  • Akita: a lightweight state management library for Angular that is inspired by the principles of the redux pattern. It provides a simple and intuitive API for managing state in Angular applications.