Creating A Simple setState() Store Using An RxJS BehaviorSubject In Angular 6.1.10
After struggling to wrap my head around Redux-inspired state management, I wanted to step back and consider simpler solutions. I'm not saying that a simpler solution is the right solution - only that moving my brain in a different direction may help me make new connections. As such, I wanted to see what it would look like to create a React-inspired state management class that exposes a .setState() method. I briefly noodled on this concept 3-years ago with AngularJS; but, with an RxJS BehaviorSubject() at my finger tips, I suspect that implementing such a state management class will be dead simple.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
A couple of days ago, I went down a rabbit-hole with RxJS streams before realizing that I was searching for the "wrong type of simplicity". In the end - after I hit rock bottom - I ended up choosing an RxJS BehaviorSubject() because it gave me everything that I needed with an easy-to-understand abstraction. Having done that, it occurred to me that the BehaviorSubject() is practically a state management class in and of itself. So, I wanted to see what it would look like to wrap the RxJS BehaviorSubject() class in a thin proxy class that exposes a .setState() method:
// Import the core angular services.
import { BehaviorSubject } from "rxjs";
import { distinctUntilChanged } from "rxjs/operators";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
export class SimpleStore<StateType = any> {
private stateSubject: BehaviorSubject<StateType>;
// I initialize the simple store with the givne initial state value.
constructor( initialState: StateType ) {
this.stateSubject = new BehaviorSubject( initialState );
}
// ---
// PUBLIC METHODS.
// ---
// I get the current state as a stream (will always emit the current state value as
// the first item in the stream).
public getState(): Observable<StateType> {
return( this.stateSubject.pipe( distinctUntilChanged() ) );
}
// I get the current state snapshot.
public getStateSnapshot() : StateType {
return( this.stateSubject.getValue() );
}
// I return the given top-level state key as a stream (will always emit the current
// key value as the first item in the stream).
public select<K extends keyof StateType>( key: K ) : Observable<StateType[K]> {
var selectStream = this.stateSubject.pipe(
map(
( state: StateType ) => {
return( state[ key ] );
}
),
distinctUntilChanged()
);
return( selectStream );
}
// I move the store to a new state by merging the given partial state into the
// existing state (creating a new state object).
// --
// CAUTION: Partial<T> does not currently project against "undefined" values. This is
// a known type safety issue in TypeScript.
public setState( partialState: Partial<StateType> ) : void {
var currentState = this.getStateSnapshot();
var nextState = Object.assign( {}, currentState, partialState );
this.stateSubject.next( nextState );
}
}
As you can see, this class does little more than delegate calls to the underlying BehaviorSubject(). In fact, the .setState() method is simply assembling the next value to emit on the BehaviorSubject() stream.
Now, let's look at how this can be consumed. I've put together a simple Angular 6.1.10 demo that creates a SimpleStore class for baby names. Then, subscribes to the names before trying to change them:
// Import the core angular services.
import { Component } from "@angular/core";
// Import the application components and services.
import { SimpleStore } from "./simple.store";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
interface NameStore {
girl: string;
boy: string;
};
@Component({
selector: "my-app",
styleUrls: [ "./app.component.less" ],
template:
`
<em>See console output</em>.
`
})
export class AppComponent {
// I initialize the app component.
constructor() {
// Create a simple store for baby name selection.
var babyNames = new SimpleStore<NameStore>({
girl: "Jill",
boy: "John"
});
// --
// Subscribe to any changes in the state (a new state object is created every
// time setState() is called).
babyNames.getState().subscribe(
( state ) => {
console.log( "New state..." );
}
);
// Subscribe to the individual name selections. Since these are unique changes,
// these callbacks will be called far less often than the getState() stream.
babyNames.select( "girl" ).subscribe(
( name ) => {
console.log( "Girl's Name:", name );
}
);
babyNames.select( "boy" ).subscribe(
( name ) => {
console.log( "Boy's Name:", name );
}
);
// --
// Try changing up some state!
babyNames.setState({
girl: "Kim"
});
babyNames.setState({
girl: "Kim" // Duplicate.
});
babyNames.setState({
girl: "Kim", // Duplicate.
boy: "Tim"
});
babyNames.setState({
girl: "Kim" // Duplicate.
});
babyNames.setState({
girl: "Joanna"
});
babyNames.setState({
girl: "Joanna" // Duplicate.
});
babyNames.setState({
girl: "Joanna" // Duplicate.
});
}
}
In this code, we're subscribing to the top-level state as well as the individual names. A new top-level state object gets created every time we call .setState(); so, we expect the top-level observer to be called a lot. But, when we subscribe to the individual state keys (ie, names), our streams end with a distinctUntilChanged() operator. As such, these observer callbacks should only be invoked when the actual values change.
And, if we run this Angular application in the browser, we get the following output:
As you can see, our getState() subscription callback is invoked every time the .setState() method is called. However, since our name-based streams are "distinct until changed", the key-level subscription callbacks are only invoked when the actual name values change.
I don't know how a simple state store like this fits into a wider state management picture. But, I'm having trouble creating a larger mental model for state management. As such, I think it's helpful to drop-down and solve smaller problems such that I may have the building blocks needed for a larger mental model. If nothing else, this builds a greater appreciation for RxJS BehaviorSubject() streams, which have proven to be very useful.
Want to use code from this post? Check out the license.
Reader Comments
@All,
Having just posted this, it just occurred to me that the
.setState()
method in React can actually accept a callback so that you can get access to the existing state while trying to calculate the next state. It would look something like this:D'oh -- hit the submit too quickly. What I meant to say was that it would be relatively easy to update the
.setState()
method to allow for either aPartial<T>
or a function that would return the partial.I'll see if I can put that together quickly later.
You should check out Akita. It basically does the same thing, but more.
https://github.com/datorama/akita
@All,
I just wanted to quickly jump in with an update that allows the
.setState()
method to accept either aPartial<T>
or a callback. It looks something like this:Read more here: www.bennadel.com/blog/3523-creating-a-simple-setstate-store-using-an-rxjs-behaviorsubject-in-angular-6-1-10---part-2.htm
@Netanel,
A couple of people have recommended Akita to me so far. I will definitely take a look. There was some complexity in it that felt weird to me, but I don't remember what it was. Maybe something about models or entities or something. But, I'm still trying to wrap my head around all of this - I am sure once I sort out some things in my head, I'll be able to return to Akita and make more sense of it.
Sometimes, I have to bang my head against the wall a bunch before I understand what other people are doing :D
@Ben,
Cool, if you have any questions, let me know :)
@All,
I've put together my first experiment to help me flesh out some ideas around storage:
www.bennadel.com/blog/3526-experimenting-with-the-runtime-abstraction-for-state-management-in-angular-7-0-3.htm
.... this uses what I'm calling the Runtime Abstraction, which encapsulates the kind of
SimpleStore
class that I outlined in this post.Hey, your implemantaion is great!
Just a quick question ? how can I 'select' an INNER field of an object.
lets say that I have a state
export class GlobalState {
}
now when I getState().select('HERE I GET ONLY data') ...
Is it possible to allow the select to be deep ?
@NSN,
" now when I getState().select('HERE I GET ONLY data') "
actually is .select('....') without getState() ofcourse.
@Ben,
Hey, since its not possible to edit, I just want to clear myself
I look for something like
.select('data.someInnerFieldOfData').subscribe()
is this even possible ?
@NSN,
Good question. I actually tried to implement something like that in my first approach. I don't think it was possible with a single string (that contains dots); but, I think I may have gotten something to work if you pass in an individual argument for each traversal operation. Something like:
Then, internally, I had something like:
I am not sure if I got it working or not. I think maybe I got something working; but, it looked really janky internally. And, frankly, I'm not that good with RxJS. If you want to see how crazy the TypeScript can get, take a look at the NgRx "selector" class:
https://github.com/ngrx/platform/blob/master/modules/store/src/selector.ts
.... pretty crazy!
Ultimately, I abandoned it because I am not a fan of having really deeply nested state. My applications have several stores -- not just a single Redux-style Store that contains "all the things". As such, I don't really get much friction from having to poke at store objects. Usually just one or two keys at most. But, that's just my personal approach.
@Ben,
"Ultimately, I abandoned it because I am not a fan of having really deeply nested state. My applications have several stores -"
So i guess I work just fine.
Its just that sometimes the state becomes big with alots of fields..
How do you keep it small ?
@NSN,
To be honest, I don't have a lot of experience with Store. I am developing my own personal style as I dig into all of this stuff. As you can see, this post itself is only a few months old.
That said, I try to keep my Stores oriented around a "feature" of the application, not the application itself. So, I might have an "authentication" store or a "user list" store. Then, the amount of data in each store is implicitly limited by the scope of the feature.
I also wrap my Store inside something I call a "Runtime", which is really just the encapsulation of a Store for a particular feature. So, going back to the "authentication" store, really I would have an
AuthenticationRuntime
that wrapsStore
. Then, it would be up to theAuthenticationRuntime
to expose RxJS streams for underlying properties.I talk more thoroughly about this "Runtime" abstraction in a post from a few weeks ago:
www.bennadel.com/blog/3584-some-real-world-experimenting-with-the-runtime-abstraction-for-state-management-in-angular-7-2-7.htm
But, going back to your "select" question, the Runtime provides me with a translation layer to get at those inner keys. So, for example, if the Runtime needed to expose a stream for
data.someInnerValue
, it might have a method like:So, essentially that path traversal in moved from the "Store" into the "Runtime". This way, the calling context (ie, your components) still don't have to worry about the complexity and they just consume the targeted value without having to worry how it's stored in the underlying state.
Sorry, that's probably a lot to take in -- and, it likely different than a lot of the state management stuff you've seen out in the wild. I'm trying to find patterns that my brain can make sense of without have to have a masters degree in RxJS :D I see too many stream operators and my brain melts. So, I am trying to find ways to leverage Streams without making them too much of a hurdle.
@NSN,
Oh, and I forgot that
.map()
has been replaced with.pipe( map() )
. Just writing stuff off-the-cuff here.@Ben,
Thanks for your answers!
another one ? :)
what about sharing the subscription correctly ?
@NSN,
So, sharing a subscription, from my understanding of RxJS, has more to with "Hot" vs "Cold" streams. In this case, The
SimpleStore
doesn't own the source of the data - it just manages the internalBehaviorSubject
. As such, there's no sense that it can prevent data from being moved around until someone Subscribes to a stream.In other words, calls to
.setState()
are going to happen regardless of whether or not anyone is subscribed to the state stream. So, essentially, theSmipleStore
is a hot stream of data that's flowing regardless of subscription state. This means that you don't have to worry about sharing subscriptions because people will just get whatever data comes through after they subscribe.Now, because the underlying structure is a
BehaviorSubject
, it does remember the last pushed value. So, to that end, every subscription will get the last pushed value, even if that value was pushed prior to the subscription.Hope that makes sense. My mental model for RxJS is not super strong - in fact, I think the RxJS community is moving away from the terms "Hot" and "Cold" re: streams. So, take what I am saying with some caution :D
@Ben ,
How can I setState for InnerValues ?
@NSN,
Assuming you would want the parent
data
to be immutable, then I would just expose a method that overwritesdata
with a newinnerValue
property. Something like:It's a bit tedious, but not overly complex. Essentially, you are creating a shallow-copy of
data
and then appending the newinnerValue
property. This is the same general approach that frameworks like Redux use.The shallower you can keep your Store, the easier this kind of stuff becomes.
@Ben,
Can It be more Generic ?
Like you did with the select method ?
@Ben
I managed to do something like that
setInnerValue<K extends keyof StateType, S extends keyof StateType[K], T extends keyof StateType[K][S]>(newVal:any, mainKey: K, secKey: S, thirdKey?: T) {
}
but this is good only for 2/3 inner fields, How can I make this more generic ?
@NSN,
This is getting a bit over my head :D RxJS is still something I am struggling with. Even some of the TypeScript here is getting over my head (all the
extends
kind of things are hard for my brain to process). As far as why the stream isn't updating, it's not obvious to me. It looks like you are creating new objects, so it should see that the reference changed.Hey Ben,
Currently I am working on this lib: https://github.com/spierala/mini-rx-store
It offers a simple feature based API with setState and select. Maybe it could be useful for you.
Behind the scenes it uses the Redux pattern. So if you want you can also do the big redux stuff like Actions, Reducers, Dispatch Action, Memoized State Selectors for global state management.
I would highly appreciate if you let me know what you think about it.
It is still beta and maybe the API could still be improved.
Thanks,
Florian
Hi Ben,
I have implemented this store exactly same way. Now when i subscribe any value from store and do some operation like the original value getting change in the store as well. Please find the sample code snippet below.
Is there any simple fix in store implementation for this?
this.commonService.store
.select(CommonStoreType.UserCustomGroupPreferences)
.subscribe((element: any) => {
if (element) {
//let preference = [...element].splice(2,2); // This clone object does not update the original store value
let preference = element.splice(2,2); // But this line change the original store value
posted this article based on some ideas of Ben on dev.to:
https://dev.to/angular/simple-yet-powerful-state-management-in-angular-with-rxjs-4f8g
TLDR Let's create our own state management Class with just RxJS/BehaviorSubject (inspired by some well known state management libs).
NgRx team reacted the second state management library that has the same concept: @ngrx/component-store
Basically, that's a "Service with a BehaviorSubject" only better organized 🙂
Check it out: https://ngrx.io/guide/component-store
*created
:)
Hi all. I read some of the comments posted here. I am using a similar implementation instead of a redux store. My application is from before i discovered various implementations of redux. 2018 ish. I see there is something called a component store in ngrx now, but i still prefer to have my own implementation as this is a hobby project. Therefor is there any updates or expansion of this project that i can follow?