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,
});
}
}
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 whenstate$
emits a new state. Within RxJSmap
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 theselect
method returns an Observable (and not anAnonymousSubject
). -
setState
accepts a Partial Type. This allows us to be lazy and pass only some properties of a bigger state interface. Inside thestate$.next
method the partial state is merged with the full state object. Finally the BehaviorSubjectthis.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 });
}
}
Let’s go through the TodosStateService Code:
- The TodosStateService extends
StateService
and passes the state interfaceTodoState
- The constructor needs to call
super()
and pass the initial state - The public Observables
todos$
andselectedTodo$
expose the corresponding state data to interested consumers like components or other services - The public methods
addTodo
andselectTodo
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:
- 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
});
}
}
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>
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})
);
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:
- Simple state management in Angular with only Services and RxJS by Aslan Vatsaev
- Very similar approach: Creating A Simple setState() Store Using An RxJS BehaviorSubject In Angular 6.1.10 by Ben Nadel
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.
- 🤓 Learn more about MiniRx on the Docs site
- ⭐ MiniRx on GitHub
- 🚀 Angular Demo
Top comments (26)
Good article, Florian.
If you add:
effect
to manage side-effectsunsubscribe
when this service is destroyedBehaviorSubject
withReplaySubject(1)
to allow the state to be initialized lazilythen 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...
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 inselect()
(I think).The workaround is to set it to
false
once it is used in the subscription like so:Do you have another suggestion?
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.
Excellent suggestion! Thank you.
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 :)
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
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.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:
and you want to know when only the value of the
city
property will change. You wouldn't be able to achieve that, becausedistinctUntilChanged
uses a strict comparsion by default, so the state subject will emit the same value of thecity
property every time theuser
changes. Sometimes this can lead to unwanted behaviour, such as requesting data.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 theselect
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);
Really great, thanks a lot - it's such a nice approach that allows to circuit the ngrx-dreadnought in smaller projects :)
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
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 :)
Love this approach, implementing it as I type :)
Plus 100 for RxJS
Finally people waking up to the power of reactive programming and streams.
Hi! How do I delete by name an item from the state and notify to the subscriptions? Thanks
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).
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?
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.