loading...
Cover image for Simple yet powerful state management in Angular with RxJS
Angular

Simple yet powerful state management in Angular with RxJS

spierala profile image Florian Spier Updated on ・7 min read

TLDR Let’s create our own state management Class with just RxJS/BehaviorSubject (inspired by some well known state management libs).

Manage state with RxJS BehaviorSubject

There are several great state management libraries out there to manage state in Angular: E.g. NgRx, Akita or NgXs. They all have one thing in common: They are based on RxJS Observables and the state is stored in a special kind of Observable: The BehaviorSubject.

Why RxJS Observables?

  • Observables are first class citizens in Angular. Many of the core functionalities of Angular have an RxJS implementation (e.g. HttpClient, Forms, Router and more). Managing state with Observables integrates nicely with the rest of the Angular ecosystem.
  • With Observables it is easy to inform Components about state changes. Components can subscribe to Observables which hold the state. These "State" Observables emit a new value when state changes.

What is special about BehaviorSubject?

  • A BehaviorSubject emits its last emitted value to new/late subscribers
  • It has an initial value
  • Its current value can be accessed via the getValue method
  • A new value can be emitted using the next method
  • A BehaviorSubject is multicast: Internally it holds a list of all subscribers. All subscribers share the same Observable execution. When the BehaviorSubject emits a new value then the exact same value is pushed to all subscribers.

Our own state management with BehaviorSubject

So if all the big state management libs are using RxJS BehaviorSubject and Angular comes with RxJS out of the box... Can we create our own state management with just Angular Services and BehaviorSubject?

Let's create a simple yet powerful state management Class which can be extended by Angular services.

The key goals are:

  • Be able to define a state interface and set initial state
  • Straight forward API to update state and select state: setState, select
  • Selected state should be returned as an Observable. The Observable emits when selected state changes.
  • Be able to use ChangeDetectionStrategy.OnPush in our Components for better performance (read more on OnPush here: "A Comprehensive Guide to Angular onPush Change Detection Strategy").

The solution:

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,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Let’s have a closer look at the code above:

  • The StateService expects a generic type T representing the state interface. This type is passed when extending the StateService.
  • get state() returns the current state snapshot
  • The constructor takes an initial state and initializes the BehaviorSubject.
  • select takes a callback function. That function is called when state$ emits a new state. Within RxJS map the callback function will return a piece of state. distinctUntilChanged will skip emissions until the selected piece of state holds a new value/object reference. this.state$.asObservable() makes sure that the select method returns an Observable (and not an AnonymousSubject).
  • setState accepts a Partial Type. This allows us to be lazy and pass only some properties of a bigger state interface. Inside the state$.next method the partial state is merged with the full state object. Finally the BehaviorSubject this.state$ will emit a brand new state object.

Usage

Angular Services which have to manage some state can simply extend the StateService to select and update state.

There is only one thing in the world to manage: TODOS! :) Let’s create a TodosStateService.

interface TodoState {
  todos: Todo[];
  selectedTodoId: number;
}

const initialState: TodoState = {
  todos: [],
  selectedTodoId: undefined
};

@Injectable({
  providedIn: 'root'
})
export class TodosStateService extends StateService<TodoState>{
  todos$: Observable<Todo[]> = this.select(state => state.todos);

  selectedTodo$: Observable<Todo> = this.select((state) => {
    return state.todos.find((item) => item.id === state.selectedTodoId);
  });

  constructor() {
    super(initialState);
  }

  addTodo(todo: Todo) {
    this.setState({todos: [...this.state.todos, todo]})
  }

  selectTodo(todo: Todo) {
    this.setState({ selectedTodoId: todo.id });
  }
}
Enter fullscreen mode Exit fullscreen mode

Let’s go through the TodosStateService Code:

  • The TodosStateService extends StateService and passes the state interface TodoState
  • The constructor needs to call super() and pass the initial state
  • The public Observables todos$ and selectedTodo$ expose the corresponding state data to interested consumers like components or other services
  • The public methods addTodo and selectTodo expose a public API to update state.

Interaction with Components and Backend API

Let’s see how we can integrate our TodosStateService with Angular Components and a Backend API:

Alt Text

  • Components call public methods of the TodosStateService to update state
  • Components interested in state simply subscribe to the corresponding public Observables which are exposed by the TodosStateService.
  • API calls are closely related to state. Quite often an API response will directly update the state. Therefore API calls are triggered by the TodosStateService. Once an API call has completed the state can be updated straight away using setState

Demo

See a full blown TODOs App using the TodosStateService:
Stackblitz - Angular State Manager

Notes

Immutable Data

To benefit from ChangeDetectionStrategy.OnPush in our components we have to make sure to NOT mutate the state.
It is our responsibility to always pass a new object to the setState method. If we want to update a nested property which holds an object/array, then we have to assign a new object/array as well.

See the complete TodosStateService (on Stackblitz) for more examples of immutable state updates.

FYI
There are libs which can help you to keep the state data immutable:
Immer
ImmutableJS

async pipe for Subscriptions

In most cases components should subscribe to the "State" Observables using the async pipe in the template. The async pipe subscribes for us and will handle unsubscribing automatically when the component is destroyed.

There is one more benefit of the async pipe:
When components use the OnPush Change Detection Strategy they will update their View only in these cases automatically:

  • if an @Input receives a new value/object reference
  • if a DOM event is triggered from the component or one of its children

There are situations where the component has neither a DOM event nor an @Input that changes. If that component subscribed to state changes inside the component Class, then the Angular Change Detection will not know that the View needs to be updated once the observed state emits.

You might fix it by using ChangeDetectorRef.markForCheck(). It tells the ChangeDetector to check for state changes anyway (in the current or next Change Detection Cycle) and update the View if necessary.

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoShellComponent {
  todos: Todo[];

  constructor(
    private todosState: TodosStateService,
    private cdr: ChangeDetectorRef
  ) {
    this.todosState.todos$.subscribe(todos => {
      this.todos = todos;
      this.cdr.markForCheck(); // Fix View not updating
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

But we can also use the async pipe in the template instead. It is calling ChangeDetectorRef.markForCheck for us. See here in the Angular Source: async_pipe

Much shorter and prettier:

<todo-list [todos]="todos$ | async"></todo-list>
Enter fullscreen mode Exit fullscreen mode

The async pipe does a lot. Subscribe, unsubscribe, markForCheck. Let's use it where possible.

See the async pipe in action in the Demo: todo-shell.component.html

select callbacks are called often

We should be aware of the fact that a callback passed to the select method needs to be executed on every call to setState.
Therefore the select callback should not contain heavy calculations.

Multicasting is gone

If there are many subscribers to an Observable which is returned by the select method then we see something interesting: The Multicasting of BehaviorSubject is gone... The callback function passed to the select method is called multiple times when state changes. The Observable is executed per subscriber.
This is because we converted the BehaviorSubject to an Observable using this.state$.asObservable(). Observables do not multicast.

Luckily RxJS provides an (multicasting) operator to make an Observable multicast: shareReplay.

I would suggest to use the shareReplay operator only where it's needed. Let's assume there are multiple subscribers to the todos$ Observable. In that case we could make it multicast like this:

todos$: Observable<Todo[]> = this.select(state => state.todos).pipe(
    shareReplay({refCount: true, bufferSize: 1})
);
Enter fullscreen mode Exit fullscreen mode

It is important to use refCount: true to avoid memory leaks. bufferSize: 1 will make sure that late subscribers still get the last emitted value.

Read more about multicasting operators here: The magic of RXJS sharing operators and their differences

Facade Pattern

There is one more nice thing. The state management service promotes the facade pattern: select and setState are protected functions. Therefore they can only be called inside the TodosStateService. This helps to keep components lean and clean, since they will not be able to use the setState/select methods directly (e.g. on a injected TodosStateService). State implementation details stay inside the TodosStateService.
The facade pattern makes it easy to refactor the TodosStateService to another state management solution (e.g. NgRx) - if you ever want to :)

Thanks

Special thanks for reviewing this blog post:

Articles which inspired me:

Posted on by:

spierala profile

Florian Spier

@spierala

Senior Frontend Dev with focus on Angular

Angular

This is where we write about all things Angular. It's meant to be a place for Angular community and people interested in Angular and the Angular ecosystem.

Discussion

pic
Editor guide
 

Good article, Florian.
If you add:

  • effect to manage side-effects
  • unsubscribe when this service is destroyed
  • replace BehaviorSubject with ReplaySubject(1) to allow the state to be initialized lazily

then you'd pretty much re-implement @ngrx/component-store :) (ngrx.io/guide/component-store)

Check out the source: github.com/ngrx/platform/blob/mast...

 

Thanks, that's interesting! ReplaySubject... And I thought every Store uses BehaviorSubject ;)
Still for an DIY StateService I think BehaviorSubject is the most straightforward option.

Regarding unsubscribe:
Maybe I can clarify in the post that the services which extend the StateService are supposed to have the lifespan of the application. If such a service would have the lifespan of a component then it should have an option to unsubscribe.

effect is cool!

 

The problem with DIY is that many of the cases are overlooked (and could be error-prone) 😉
What's better than a well-tested tiny lib that handles these for you? 😃

Btw, I typically try to caution about such services that live for the lifespan of the app (unless it's the Globally managed store) - even though I list it as one of the use cases (ngrx.io/guide/component-store/usag... - still working on the docs).
It's very easy to loose track of things.

Is @ngrx/component-store supposed to be used also in long living (app lifespan) singleton services? I thought that state with the lifespan of the app would be more a use case for @ngrx/store.

I will ask you on discord :)

 

Thanks Florian, I need to read this over again, and again. Reason: I'm not sold on farming off state management as this is only a recent concept with redux etc.

A few Questions if you don't mind

Do you find State Management as its own concern improves our developer lives? Does it make the whole state thing go smoother, faster, easier? How do this tie in with FormControls in Angular?

 

Hi John. At least my developers life became more fun with state management. I started out with NgRx and was quite happy with it. In NgRx you also work with immutable data and state changes happen explicitly (with dispatching an Action). In the simple StateService in this article we have a similar explicitness with using setState inside public API methods. That helps to understand/debug where certain state changes come from.

In NgRx you have the principle of Single Source of Truth. It means that there is just one place (Store) which holds the complete application state object. That way you always know where to find/query the state data. The simple StateService has a similar purpose. Services which extend the StateService are also the Single Source of Truth for a specific feature (e.g. the TodosStateService is the Single Source of Truth for everything related to Todos).

With immutable data and Observables it is easily possible to use ChangeDetectionStrategy.OnPush which will improve performance (if you have a lot of components).

Also when working in a Team it is great to have a state management solution in place, just to have a consistent way of updating/reading state that every one can simply follow.

Regarding Form Controls... Ideally the component which holds the form does not know about state management details. The form data could flow into the form component with an @Input() and flow out with an @Output() when submitting the form. But if you use a Facade then the form has no chance to know about state management details anyway.
There is one important thing to keep in mind: Template Driven Forms which use two-way-binding with [(ngModel)] can mutate the state. So you should use one-way-binding with [ngModel] or go for Reactive Forms.

 

I'll let OP reply to other questions but to tie some data to an angular form you could give a go to dev.to/maxime1992/building-scalabl...

 

I thought it would be pretty cool to build a state management system similar to NGXS/NGRX and Redux using just Rxjs. I essentially used the same concepts but wrapped it in a service and framework agnostic way.
npmjs.com/package/@jwhenry/rx-state
I took from both NGXS and Redux and came up with this idea. It's not as robust as the big boys, but it'll get the job done for small projects.

 

Hi Justin, nice lib! Yeah I know it is tempting to write your own state management solution with RxJS :) RxJS gives you a great foundation to start off. E.g. with the scan operator and a few more lines of code you almost have a (basic) NgRx Store: How I wrote NgRx Store in 63 lines of code
With RxJS you can easily write the state management of your dreams :)

 

Plus 100 for RxJS
Finally people waking up to the power of reactive programming and streams.