DEV Community

Cover image for Redux with Observable Stores in Angular
Stephen Belovarich
Stephen Belovarich

Posted on

Redux with Observable Stores in Angular

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.

NgRx kinda felt like this

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

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);
  }

}
Enter fullscreen mode Exit fullscreen mode

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());
  }
}

Enter fullscreen mode Exit fullscreen mode

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) {}
}

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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[];
}
Enter fullscreen mode Exit fullscreen mode

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

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

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;
  }

Enter fullscreen mode Exit fullscreen mode

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

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

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

In the reducer, passing in the REPLACE action will overwrite the state.

case LocalActions.REPLACE:
  return {
    ...state,
    someModel: action.payload
  };
Enter fullscreen mode Exit fullscreen mode

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

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>
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
steveblue profile image
Stephen Belovarich

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.

Collapse
 
aziziyazit profile image
Azizi Yazit

I've used Observable.store in 2 in my freelance projects. Really love the simplicity it offered.

Collapse
 
rconr007 profile image
rconr007

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.

Collapse
 
webmarket7 profile image
Oleksandr Ryzhyk

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!

Collapse
 
steveblue profile image
Stephen Belovarich

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!

Collapse
 
christhebutcher profile image
ChrisTheButcher

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.

Collapse
 
crh225 profile image
Chris House

Do you have a git repo with this example?

Collapse
 
dddsuzuki profile image
dddsuzuki

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?

Collapse
 
steveblue profile image
Stephen Belovarich

In this simple expression of an Observable store you can do something like this in a template.

{{ (store.state$ | async).chunk }}

Collapse
 
johntday profile image
John T Day

I've missed something. what is AbstractAction ?

Collapse
 
steveblue profile image
Stephen Belovarich

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.