Angular State Management: how we replaced NgRx with our own system

Anton Filipovich
4 min readFeb 20, 2023

--

Photo by Claudia Chiavazza on Unsplash

Hello. I’m Anton, a Frontend Team Lead, and I’m going to share how in my team we solved the problem of state management in an Angular application.

Many articles have been written about state management in Angular applications, and most likely you have heard about the NgRx or BehaviorSubject approach. Initially, we chose between them, but later we came to create our own tool.

The reason to come up with something new in this direction is the balance between features and ease of use — NgRx has lots of features but also is hard to use, BehaviorSubject is simple but has a lack of features.

NgRx

NgRx is a very powerful tool for state management. It is actually a Redux adaptation for Angular.

But it has some huge cons — a lot of boilerplate code and difficulties with understanding, especially for your colleagues who have never worked with it before.

To create, at least, a basic flow such as loading some data from a server, you need to implement all of this:

  1. Actions
  2. Reducers
  3. Selectors
  4. Effects

BehaviorSubject

Classic Angular state management approach. You create a service containing a BehaviorSubject and store all the data inside it.

// data.service.ts
@Injectable({provideIn: 'root'})
class DataService {
data$ = new BehaviourSubject({ foo: 'bar' });
}

// app.component.ts
@Component({
// ...
})
export class AppComponent {
// Getting the data
foo$ = this.dataService.data$.asObservable().pipe(map(data => data.foo));

constructor(dataService: DataService) {
}

// Setting new data
updateData(foo: string) {
this.dataService.data$.next({ foo });
}
}

This approach is much simpler, but has the following problems:

  • The state is mutable and can be changed implicitly.
  • State updates come uncontrollably throughout the app — there’s nothing similar to reducers.

Let’s combine approaches

The new approach should have two main things:

  • Encapsulated and immutable state.
  • All state manipulations are restricted and handled from one place.
A lifecycle we are going to implement

Initially, let’s create a service with a BehaviourSubject

export class StateService {
private state: BehaviorSubject<State> = new BehaviorSubject<State>(initialState);
}

We will need some getters for the state.

export class StateService {
private state: BehaviorSubject<State> = new BehaviorSubject<State>(initialState);

// Get state as an Observable
get state$(): Observable<State> {
return this.state.asObservable();
}
}

Also, sometimes there’s a need to just retrieve the current state value without Observables. To handle these cases let’s add a stateCurrentValue getter.

export class StateService {
private state: BehaviorSubject<State> = new BehaviorSubject<State>(initialState);

// Get state as an Observable
get state$(): Observable<State> {
return this.state.asObservable();
}

// Get state current value as a plain object
get stateCurrentValue(): State {
return this.state.getValue();
}
}

Now let’s create a way to update the state. It should stay immutable, so we will create a new object each time. Also, to prevent any workarounds with indirect state mutation, we will freeze the state.

export class StateService {
private state: BehaviorSubject<State> = new BehaviorSubject<State>(initialState);

// Get state as an Observable
get state$(): Observable<State> {
return this.state.asObservable();
}

// Get state current value as a plain object
get stateCurrentValue(): State {
return this.state.getValue();
}

// Set new state
setState(newState: State): void {
this.state.next(Object.freeze({...newState}));
}
}

Actually, you don’t need to retrieve the entire state all the time, often you need to take just a part of it. It can easily be solved by a simple rxjs map operator, but there is a problem. Each state update emits a new value from state$ Observable, even if a part of it was never changed. So it will trigger many unnecessary updates in your component.

To handle this case and prevent unnecessary updates, let’s implement a select method that only emits a value when something has changed on the state side.

export class StateService {
private state: BehaviorSubject<State> = new BehaviorSubject<State>(initialState);

// Get state as an Observable
get state$(): Observable<State> {
return this.state.asObservable();
}

// Get state current value as a plain object
get stateCurrentValue(): State {
return clone(this.state.getValue());
}

// Set new state
setState(newState: State): void {
this.state.next(Object.freeze({...newState}));
}

// Select a part of the state
select<K>(mapFn: (state: State) => K): Observable<K> {
return this.state$.pipe(
map(mapFn),
distinctUntilChanged((a: K, b: K) => equal(a, b))
);
}
}

Hooray! Now we have it. We have a working StateService. Now we need a StateActionsService — for example, to add and remove some users.

export class StateActionsService {
constructor(private state: StateService) { }

addNewUser(user: User): void {
const {users} = this.state.stateCurrentValue;
this.state.setState({
users: users.concat(user)
});
}

removeUser(index: number): void {
const {users} = this.state.stateCurrentValue;
this.state.setState({
users: users.slice(0, index).concat(users.slice(index + 1))
});
}
}

Now let’s use it all inside a component:

export class AppComponent {

form = this.fb.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required],
age: [0, Validators.required],
});

users$ = this.state.select(({users}) => users);

constructor(
private fb: FormBuilder,
private state: StateService,
private stateActions: StateActionsService
) {
}

addNewUser(): void {
this.stateActions.addNewUser(this.form.value as User);
this.form.reset();
}

removeUser(index: number): void {
this.stateActions.removeUser(index);
}

}

And finally, we have got a working state management system with the main benefits of Redux — single source of truth, immutability, changing the state only in a given set of ways, but also it appears to be quite simple to understand and use.

I hope this article will help you to learn something new about state management.

--

--