It's turning out that 2019 is the year of the Observable store at ng-conf with several speakers advocating for this pattern in Angular apps.
I recently hopped off a large project that used NgRx for state management in Angular and have to say I was largely overwhelmed at first. A common complaint with NgRx is that it requires a lot of boilerplate. It's true that maintaining a separation of concerns can get out of hand when implementing selectors, actions, reducers and effects in NgRx. It can be very hard for a noob to understand how NgRx implements redux, let alone handle all the files that can be produced. Even seasoned senior engineers can be humbled by the experience.
Redux doesn't have to be this complicated. The purpose of Redux is to simplify state management in complex applications with a pattern for unidirectional data flow. Keeping the pattern simple has some advantages.
- Those unfamiliar with Redux can ramp up faster
- Scale faster with less boilerplate
- Not using another library will make the bundle smaller
- Control how state management behaves
RxJS BehaviorSubject
It so happens NgRx isn't the only way you can implement a redux pattern in Angular. We already have tools at our disposal in RxJS that allow us to create a store with Observables. The pattern I'm talking about is called Observable stores. The simplest expression of an Observable store looks like this.
this._state$ = new BehaviorSubject(initialState);
RxJS has BehaviorSubject
which essentially gives us the API of an Observable, but also maintains state as well. BehaviorSubject
takes an initial state.
Observable Store
If we wanted to abstract the ability to create an Observable store in an application it could look like this.
export interface AbstractState {
[key: string]: any;
}
export class Store {
private _state$: BehaviorSubject<AbstractState>;
public state$: Observable<AbstractState>;
constructor (initialState: AbstractState) {
this._state$ = new BehaviorSubject(initialState);
this.state$ = this._state$.asObservable() as Observable<AbstractState>;
}
get state(): AbstractState {
return this._state$.getValue();
}
setState (nextState: AbstractState): void {
this._state$.next(nextState);
}
}
That's really all there is to abstracting an Observable store!
The Store class has a private property that is the BehaviorSubject. A property called state$
is exposed publicly for us to use throughout the application. We can call getState()
to retrieve state or setState
to change state. By doing it this way we retain all the characteristics of an Observable including history, error handling, all the jazz. And it's so simple compared to NgRx.
Implementing State
Then if we wanted to create some state in our application it could look like this.
export interface SomeModel {
name: string
}
export class LocalState {
someModel: SomeModel[] = [];
}
@Injectable()
export class LocalStore extends Store {
public state$: Observable<LocalState>;
constructor () {
super(new LocalState());
}
}
Some notes about the above implementation. Notice we have declared a class to handle some local state, then declared state$
for LocalStore
. This is to ensure we are working with LocalState
as opposed to AbstractState
. Then in the constructor we call super
, passing in LocalState to instantiate the BehaviorSubject
with the proper state.
Using State in Components
Now we have some local state it's time to interact with it in a component. Just inject LocalStore
and you don't even need ngOnInit
to subscribe to state changes.
export class MyComponent {
constructor(public store: LocalStore) {}
}
In the component's template you can now use state with the async
Pipe. The view will automatically be subscribed to a chunk of state this way and handle unsubscribing as well.
<ul>
<li *ngFor="let item of (store.state$ | async).someModel as SomeModel">{{item.name}}</li>
</ul>
It's really that easy to hook up an Observable store in Angular! So far we only have this idea of state, which is one part of the redux pattern. What does it look like if we wanted to implement reducers and actions? Any way we want now we have implemented our own Observable store!
Now you can get back some of that "me time" NgRx took away from you.
There's more to redux than just state. What if you wanted to manage your state with the action and reducer patterns found in redux, but in this custom implementation?
Actions and Reducers
This is just one way to implement actions and reducers and it so happens to look similar to NgRx, but with far less boilerplate.
First let's create an enum where we define actions and create an interface for what an action looks like.
export enum LocalActions {
ADD = '[SomeModel] Add',
REPLACE = '[SomeModel] Replace',
FETCH = '[SomeModel] Fetch'
}
export interface LocalAction {
type: string;
payload?: SomeModel[];
}
Now we can add a reducer method to the LocalStore to handle different actions.
reducer(state: LocalState, action?: LocalAction) {
switch (action.type) {
case LocalActions.ADD:
return {
...state,
someModel: [...state.someModel, action.payload]
};
case LocalActions.REPLACE:
return {
...state,
someModel: action.payload
};
case LocalActions.FETCH:
this._fetch$ = this.service.fetchSomeModel().pipe(
map(res => this.actions.emit({ type: LocalActions.REPLACE,
payload: res }))
).subscribe();
}
Notice the FETCH action calls a service method? To maintain a separation of concerns, we can keep all API requests on their own service and then inject that into the LocalState class.
@Injectable()
export class LocalStore extends Store {
public state$: Observable<LocalState>;
private _fetch$: Subscription;
constructor (public service: LocalService) {
super(new LocalState());
}
}
In order for LocalStore to automatically call the reducer on state change we need to update the Store class it extends from. Here we'll add a subscription to the store's actions, which we declare here as an EventEmitter so all classes that extend from store can now emit actions.
@Injectable()
export class Store {
private _subscription$: Subscription;
private _state$: BehaviorSubject<AbstractState>;
public state$: Observable<AbstractState>;
public actions: EventEmitter<AbstractAction> = new EventEmitter();
constructor (initialState: AbstractState) {
this._state$ = new BehaviorSubject(initialState);
this.state$ = this._state$.asObservable() as Observable<AbstractState>;
this._subscription$ = from(this.actions).pipe(
map((a: AbstractAction) => this.reducer(this.state, a)),
map((s: AbstractState) => this.setState(s))
).subscribe();
}
...
reducer(state: AbstractState, action?: AbstractAction) {
return state;
}
Now anywhere in our application, like in the Component we declared above we can make a backend request and populate state with the FETCH
action!
this.store.actions.emit({ type: LocalActions.FETCH });
What happened again?
Let's take the journey here to see what happens to this specific action.
In Store
the emitter we dispatched the action has a subscription that calls the reducer.
this._subscription$ = from(this.actions).pipe(
map((a: AbstractAction) => this.reducer(this.state, a)),
In the reducer, we make an http request on a service and when successful dispatch another action with the response.
case LocalActions.FETCH:
this.service.fetchSomeModel().pipe(
map(res => this.actions.emit({ type: LocalActions.REPLACE,
payload: res }))
In the reducer, passing in the REPLACE
action will overwrite the state.
case LocalActions.REPLACE:
return {
...state,
someModel: action.payload
};
Since the subscription to our EventEmitter on State also updates state by calling setState()
for us, the view will automatically pick up the changes to state.
from(this.actions).pipe(
map((a: AbstractAction) => this.reducer(this.state, a)),
map((s: AbstractState) => this.setState(s))
).subscribe();
This means in our component we only needed to dispatch an action to update the view. The async pipe handles the subscription to state for us.
<ul>
<li *ngFor="let item of (store.state$ | async).someModel as SomeModel">{{item.name}}</li>
</ul>
And there you have it! The important take away here is redux can be as simple or as complicated as you make it. By coding a mechanism for state yourself with observable stores you can understand whats going on behind the scenes with state management in rather complex libraries. Redux can be implemented in Angular with minimal boilerplate compared to NgRx and still provide a separation of concerns in our application.
But don't take my word for it.
Check out these blog posts and videos for more information about implementing Observable Stores.
Simplifying Front-End State Management with Observable Store by Dan Wahlin.
State management in Angular with observable store services by Jure Bajt.
View Facades + RxJS by Thomas Burleson.
I was first exposed to this idea of Observable stores from Thomas Burleson's post. Years ago my team architected an entire AngularJS application based off a talk he did at ng-conf. I was so happy with the results. Years later when I read his post View Facades + RxJS I chose to give service facades and Observable stores a try. I haven't looked back since. Sorry NgRx.
At ng-conf 2019 Observable stores are all the rage with multiple presentations about using RxJS to provide state to applications. As the YouTube videos are released I'll post links to them here.
Data Composition With RxJS presented by Deborah Kurata.
Top comments (11)
Yes, this is a good call and how I actually implemented it. I think I used this.state merely as a clear example in the write up.
I've used Observable.store in 2 in my freelance projects. Really love the simplicity it offered.
This is some good stuff. Thanks for this insight. I have implemented a simple store with BehaviorSubject before. But your implementation is such more clearer and cleaner. Thanks again for the post.
Great article! I love the simplicity of this approach. But I wonder, why you used EventEmitter for emitting new actions instead of Subject? AFAIK usage of EventEmitter is discouraged by Angular team for anything except sharing events between components. Was there some good reason to use EventEmitter specifically in this case? Thank you!
It’s been awhile since I looked at this but I think the rationale was exactly that, for sharing. If Subject works for you, go for it!
Great read, thanks. I've never been a fan of ngrx and I've worked with BehaviourSubjects before as well. A very nice way of implementing this pattern.
Do you have a git repo with this example?
Hello.
I love this great article.
But, I got a question.
When should I use state?
get state(): AbstractState {
return this._state$.getValue();
}
Template should not consume store.state?
In this simple expression of an Observable store you can do something like this in a template.
I've missed something. what is AbstractAction ?
AbstractAction is a placeholder interface for actions, which you can override with another interface that more clearly defines the action in any class that extends the Store class.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.