DEV Community

Cover image for Simple yet powerful state management in Angular with RxJS
Florian Spier for Angular

Posted on • Updated on

Simple yet powerful state management in Angular with RxJS

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 a 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

Template Driven Forms with two-way data binding

Regarding immutable data... We have to be careful when pushing state into a Template Driven Form where the Form inputs are using [(ngModel)]. When the user changes a Form input value then the state object will be mutated directly...
But we wanted to stay immutable and change state only explicitly using setState. Therefore it is a better alternative to use Reactive Forms. If it has to be Template Driven Forms then there is still a nice compromise: one-way data binding [ngModel]. Another option is to (deeply) clone the form data... In that case you can still use [(ngModel)].

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:

MiniRx

My experience with reactive state management made it into this very cool library: MiniRx Store.
You can easily refactor the StateService from this article to MiniRx Feature Store. MiniRx Feature Store also supports setState and select, but you get extra goodies like Redux DevTools support, undo, enforced immutability, effects, memoized selectors and much more.

Top comments (26)

Collapse
 
alexokrushko profile image
Alex Okrushko

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...

Collapse
 
spierala profile image
Florian Spier

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!

Collapse
 
alexokrushko profile image
Alex Okrushko

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.

Thread Thread
 
spierala profile image
Florian Spier • Edited

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 :)

Collapse
 
jwp profile image
John Peters • Edited

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?

Collapse
 
spierala profile image
Florian Spier • Edited

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.

Collapse
 
maxime1992 profile image
Maxime

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...

Collapse
 
brotherm profile image
MichaelC • Edited

Nice, simple approach, Florian. I'm trying it out in my current project.

One question, though: I would like to use a Boolean state object to trigger an action in another component when the value is true. Unfortunately, it only works the first time because of the distinctUntilChanged() operator in select() (I think).

The workaround is to set it to false once it is used in the subscription like so:

    this.appState.reloadProducts$
      .pipe(
        filter(val => val != null && val === true)
      )
      .subscribe(value => {
        this.loadProducts();
        // Reset state
        this.appState.setReloadProducts(false);
      });
Enter fullscreen mode Exit fullscreen mode
reloadProducts$: Observable<boolean> = this.select((state) => state.reloadProducts);
Enter fullscreen mode Exit fullscreen mode

Do you have another suggestion?

Collapse
 
spierala profile image
Florian Spier

I think the reload thing is not really a state, therefore I would not make it part of the state interface. It is more like an action. You can easily create an Action with an RxJS Subject and subscribe on it to trigger the API Call.
You can add that Subject to the service which extends the StateService.

Collapse
 
brotherm profile image
MichaelC

Excellent suggestion! Thank you.

Collapse
 
jwhenry3 profile image
Justin Henry

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.

Collapse
 
spierala profile image
Florian Spier

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 :)

Collapse
 
lallenfrancisl profile image
Allen Francis • Edited

One addition I think would really be helpful would be the option to specify compare function for the select for using with distinctUntilChanged. Which means the function will be

protected select<K>(
    mapFn: (state: T) => K,
    changeFn?: (a: K, key: K) => boolean
  ): Observable<K> {
    return this.state$.asObservable().pipe(
      map((state: T) => mapFn(state)),
      distinctUntilChanged(changeFn)
    );
  }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
spierala profile image
Florian Spier

What is the use-case? Normally you want the Observable returned by select to only emit when it has a new Object reference. It's the same in Akita or NgRx. That behaviour encourages also updating state in an immutable manner.

Collapse
 
killbond profile image
killbond • Edited

Sorry for necroposting, but let's say, we have two different properties in our state. Both are known to be complex, not trivial. To give a better idea, here's an example:

city: { 
  id: number, 
  name: string
},
user: {
  id: number,
  name: string,
}
Enter fullscreen mode Exit fullscreen mode

and you want to know when only the value of the city property will change. You wouldn't be able to achieve that, because distinctUntilChanged uses a strict comparsion by default, so the state subject will emit the same value of the city property every time the user changes. Sometimes this can lead to unwanted behaviour, such as requesting data.

Thread Thread
 
spierala profile image
Florian Spier

If you are interested in city changes, then you can write a selector specific for the city:

city$ = this.select(state => state.city);

The distinctUntilChanges operator inside the select method will make sure that there is only an emission if the city object changes.

However, this selector would emit when city or user change:

userAndCityState$ = this.select(state => state);

Collapse
 
w0lg0r profile image
Wolfgang Goritschnig • Edited

Really great, thanks a lot - it's such a nice approach that allows to circuit the ngrx-dreadnought in smaller projects :)

Collapse
 
snavarro89 profile image
snavarro89

Hi Florian

I've been struggling on defining my rest api and how it would interact with my state management in angular app.

Currently I have the following data in my database
Objects:
{
id
data1
data2
task: [
task1: {
array1: [
{
field1
fieldN
}
],
field1,
field2
fieldN
}
task2
task3
taskN
]
anotherArray: []
anotherArray: []
}

Basically my data has embedded objects and arrays, which in turn have nested arrays.

My problem is that I have 10 different views that need either the ObjectList or the single Object. Now this is easy I can just load all the objects with "all" or load only one with "get". But I'm struggling because it just seems unnecessary to return ALL the information as I have views where I only need the "id" and "field1" and not all the arrays with its nested arrays. Each view requires the same list with different fields, and also I have other components that can access and modify only specific objects within the nested arrays (For example I have a component to load the task and update specific fields of the task at once). I know that for this I would have to update the server and then update the store in angular after the success. But having different views that load different fields from the same "selector" methods makes it hard to maintain.

Would you recommend loading all fields at once no matter what view you are loading? Or is there something I can do at the state management to keep the data relevant to the view asking for the state (without loading everything at once from the api)?

Hopefully I was able to explain myself. I already have a working scenario, but the code is all patched up and Im reengineering what I already have working to something that is easier to maintain in the feature while I keep adding fields and/or components that read/update the same record.

Thanks

Collapse
 
spierala profile image
Florian Spier • Edited

Yeah! There is more context needed to give a good answer. But this will be quickly off-topic of this blog post.
Maybe you can come to Angular Discord and put your question there?
discord.gg/angular
Feel free to add my discord nickname to your question: @spierala

Regarding State management with the DIY StateService with immutable data: it is recommended to NOT put deeply nested data into the Store/StateService. It becomes just to painful to do the immutable updates for deeply nested objects/arrays. It is better to keep the data structure as flat as possible and setup relations with just IDs.
It is the same challenge for every state management solution which uses immutable data (e.g. NgRx).

Also you have to consider, if really all data has to go to the Store / StateService.
Sometimes it is just one component which needs a specific piece of data. It can be OK to let the component itself fetch the data and forget about the data when the component is destroyed.

It is also important to know how "fresh" the data has to be. If you always need as fresh as possible data then you need to fetch the data again when it is needed by a component.

You can map data to your specific needs e.g. in the function which fetches the data with HttpClient.get and use rxjs/map to transform the data.
Or if the data is stored already in the StateService you could create other more specialized Observables in the Service which extends StateService and use rxjs/map again.

E.g.
todosWithOnlyName$ = this.todos$.pipe(map(todo => ({name: todo.name})))

You see there are a lot of options :)

See you on Discord :)

Collapse
 
fmontes profile image
Freddy Montes

Love this approach, implementing it as I type :)

Collapse
 
anthonybrown profile image
Tony Brown

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

Collapse
 
stefanofavero profile image
Stefano

Hi! How do I delete by name an item from the state and notify to the subscriptions? Thanks

Collapse
 
spierala profile image
Florian Spier • Edited

What do you mean exactly?

Delete an item from an array by a certain criteria? (-> Array.filter will be your friend)

Delete a property from the state object? (you should not do that, but you can set the property to undefined).

When you use setState all subscriptions on the selected state Observables will be notified (if the selected state changed).

Collapse
 
stefanofavero profile image
Stefano • Edited

I mean: what if I do not need to keep a value stored anymore?

I deleted a stored key/value by storing undefined _ for the _state[key] and then a delete state[key]

Is there any different recommended procedure?

Thread Thread
 
spierala profile image
Florian Spier • Edited

Normally setting to undefined should be enough.

I would not recommend to delete properties. That might create state which is not following the state interface.

delete also mutates the object. But in the StateService we aim for immutable state updates.

Collapse
 
fireflysemantics profile image
Firefly Semantics Corporation • Edited

Awesome article!! I built a light weight state manager on top of RxJS that you may like.
npmjs.com/package/@fireflysemantic...