Handle Multiple Angular 2 Models in ngrx with Computed Observables

Intro

I am a huge fan of the simplicity and power that redux brings when dealing with state management and communication. Redux, as a software pattern, gives us a single place to store our application state (the store) and a single place to mutate our application state (the reducer). Every entity within your application model generally gets its own reducer and therefore its own unique space in the application store. So how do you handle two data models that share a relationship but are separated in the application store?

computed-observables-app

For instance, if you have a Users model and an Items model and a User has Items, how do you orchestrate that in redux? Because we are going to be using @ngrx/store as our redux implementation, we have a double rainbow situation because we not only get the power of redux but the power of observables as well. And because we have observables at our disposal, we will learn how to use Observable.combineLatest to solve this relational problem in a really elegant way.

This is not going to be a redux primer and so be sure to check out my introductory post Build a Better Angular 2 Application with Redux and ngrx if you haven’t already.

Check out the code and let’s get started!

Sample Code

TLDR

computed-observable-arrows

We can consume multiple data models from the application store and because they are wrapped in observables, use Observable.combineLatest to create a new observable that computes a new value when any of the underlying observables emit a value. In the code below, we are creating a users$ and items$ stream by selecting them from the application store. We then call Observable.combineLatest and pass in our two streams with a function that we will use to perform computations when either users$ or items$ emit a value. In this case, we are mapping over our users and creating a new object that has an items property that contains only the items that are associated with that user.

// shared/users-items.service.ts

export class UsersItemsService {
  constructor(private store: Store<AppStore>) { }
  getUsersItems(): Observable<UserItems[]> {
    const users$: Observable<User[]> = this.store.select('users');
    const items$: Observable<Item[]> = this.store.select('items');
    return Observable.combineLatest(users$, items$, (users, items) => {
      return users.map(user => Object.assign({}, user, {
        items: items.filter(item => item.userId === user.id)
      }));
    });
  }
}

The beauty of this approach is that outside of this service, the application has no idea that the result of calling getUserItems does not, in fact, live within the application store. The combined data is being calculated on the fly when the underlying models are changed and then streamed to the rest of the application.

Users Feature

users-feature

Our application will consist of two features, Users and Items, that we will combine together into a UsersItems feature. The idea is that we will start with a predefined set of users that we can add additional users to. Each user has an id property that we will use to create a relationship with an item via its userId property.

users-feature

User Model

The user object itself will be fairly simple with an id and name property that we will define in our User interface.

// shared/user.model.ts

export interface User {
  id: string;
  name: string;
}

With our interface defined, we can create an initial collection of users called initialUsers that we can use to initialize the state of our users reducer. Our reducer has a single action, ADD_USER, to handle adding a new user to the users collection.

// shared/users.service.ts

export const ADD_USER = 'ADD_USER';

export const initialUsers: User[] = [
  {id: '1', name: 'Victor Wooten'},
  {id: '2', name: 'Marcus Miller'},
  {id: '3', name: 'Jaco Pastorious'}
];

export const users: ActionReducer<User[]> = (state: User[] = initialUsers, action: Action) => {

switch (action.type) {
    case ADD_USER:
      return [...state, action.payload];
    default:
      return state;
  }
};

User Service

The UsersService serves as an intermediary between the application store and the rest of the application. When a component needs to consume the collection of users, it calls getUsers on the service which then calls store.select(‘users’); this returns a collection of users wrapped in an observable. This on its own is incredibly powerful but will become even more so when we get to our UsersItems service in a moment.

// shared/users.service.ts

export class UsersService {
  constructor(private store: Store<AppStore>) { }
  getUsers(): Observable<User[]> {
    return this.store.select('users');
  }
  initializeNewUser(): User {
    return {id: UUID.UUID(), name: ''};
  }
  addUser(user): void {
    this.store.dispatch({type: ADD_USER, payload: user});
  }
}

New users are added by calling addUser which calls store.dispatch with the appropriate action item. We also have an initializeNewUser method to return an empty user when we reset our new user form. This is a fairly simple service with the majority of its real estate dedicated to serving as the middleman between the application store and the rest of the app.

Application Module

Zooming out just a bit, in order to make an application store available, we need to tell Angular how the store needs to be initialized. We accomplish this within our NgModule definition within the imports block. Calling StoreModule.provideStore with our users and items reducers returns a configured Store service that we can then supply to the rest of the application.

// app.module.ts

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...
    StoreModule.provideStore({users, items})
  ],
  providers: [
    UsersService,
    UsersItemsService,
    ItemsService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

BONUS COMMENTARY

I want to take a quick moment and highlight what we get when we call StoreModule.provideStore with an object literal with essentially two properties that map to our users and items reducers. We get a fully initialized Store but notice that when we injected Store into our UsersService above that we typed it to Store.

// shared/users.service.ts

export class UsersService {
  constructor(private store: Store<AppStore>) { }
  // ...
}

What does AppStore do? Well, it is an interface that has a users and items property which looks surprisingly like the object that we used to initialize our application store. We can think of the AppStore interface as the master “shape” of our data, much like a database with each reducer serving as an individual table.

// app-store.model.ts

import { Item, User } from './shared';

export interface AppStore {
  users: User[];
  items: Item[];
}

The reason that I bring this up is that one of my favorite things about redux is that I no longer have to reason about having application state spread out across multiple services because it is all consolidated into a single place. Being able to define the entire shape of the application state into a single interface that had a one to one mapping with my reducers was a real “Ah ha!” moment for me.

User Component

With our UsersService serving as the middleman for our application store, we can consume the users collection in our UsersComponent by calling usersService.getUsers(). This returns an array of User objects that is wrapped in an observable which we will assign to the users$ property.

// users/users.component.ts

export class UsersComponent {
  users$: Observable<User[]> = this.usersService.getUsers();
  constructor(private usersService: UsersService) { }
}

Because users$ is an observable, we can bind to it directly in our template via *ngFor=”let user of users$ | async”. The async pipe expects either a promise or observable and handles unwrapping and accessing its underlying data which is quite convenient.

<md-list-item *ngFor="let user of users$ | async">
  <app-user [user]="user"></app-user>
</md-list-item>

We can create a new user by calling addUser, which passes in the newUser object and then re-initializes newUser to a new, blank user.

// users/users.component.ts

export class UsersComponent {
  users$: Observable<User[]> = this.usersService.getUsers();
  newUser: User = this.usersService.initializeNewUser();
  constructor(private usersService: UsersService) { }
  addUser(): void {
    this.usersService.addUser(this.newUser);
    this.newUser = this.usersService.initializeNewUser();
  }
}

Finally, we are toggling the UI for creating a new user via the shouldShowNewUser flag which gets set by calling toggleNewUser.

// users/users.component.ts

export class UsersComponent {
  users$: Observable<User[]> = this.usersService.getUsers();
  shouldShowNewUser: boolean = false;
  newUser: User = this.usersService.initializeNewUser();
  constructor(private usersService: UsersService) { }
  toggleNewUser(): void {
    this.shouldShowNewUser = !this.shouldShowNewUser;
    if (!this.shouldShowNewUser)
      this.newUser = this.usersService.initializeNewUser();
  }
  addUser(): void {
    this.usersService.addUser(this.newUser);
    this.newUser = this.usersService.initializeNewUser();
  }
}

In our template, we are using ngIf to toggle the UI for adding a new user which includes an input bound to newUser.name and a button that calls addUser when clicked.

<md-list-item *ngIf="shouldShowNewUser">
  <md-input placeholder="Name" [(ngModel)]="newUser.name">
    <button md-icon-button (click)="addUser(); $event.preventDefault();" md-suffix>
      <md-icon class="md-24">add_circle</md-icon>
    </button>
  </md-input>
</md-list-item>

Now that we have arrived at our template, we are ready to move on to the Items feature. Pay special attention to how similar the pieces are along the way.

Items Feature

items-feature

We will use the Items feature to create a relationship between two models within our application by associating an item to a specific user.

items-feature

Items Model

An Item object is fairly simplistic in that it contains an id and name property with the notable addition of the userId property which is basically a foreign key that points to a specific user.

// shared/item.model.ts

export interface Item {
  id: string;
  name: string;
  userId: string;
}

Our items reducer is initialized with our initialItems collection and has a single ADD_ITEM action selector that adds an item to the collection.

// shared/items.service.ts

export const ADD_ITEM = 'ADD_ITEM';

export const initialItems: Item[] = [
  {id: '1', name: 'Item 1', userId: '3'},
  {id: '2', name: 'Item 2', userId: '2'},
  {id: '3', name: 'Item 3', userId: '1'}
];

export const items: ActionReducer<Item[]> = (state: Item[] = initialItems, action: Action) => {
  switch (action.type) {
    case ADD_ITEM:
      return [...state, action.payload];
    default:
      return state;
  }
};

Items Service

The ItemsService exposes the items collection with a getItems method that calls store.select(‘items’). We can add a new item by calling addItem with a freshly minted item which gets added to the payload of an Action object and sent up the stream to the items reducer.

// shared/items.service.ts

export class ItemsService {
  constructor(private store: Store<AppStore>) { }
  getItems(): Observable<Item[]> {
    return this.store.select('items');
  }
  initializeNewItem(): Item {
    return {id: UUID.UUID(), name: '', userId: undefined};
  }
  addItem(item): void {
    this.store.dispatch({type: ADD_ITEM, payload: item});
  }
}

Items Component

Because we have an additional moving piece (users) when we create a new item, our ItemsComponent will contain a reference to the items and users collections. The pattern is exactly the same in that we inject our services, call the appropriate getter and store the observable that is returned.

export class ItemsComponent {
  items$: Observable<Item[]> = this.itemsService.getItems();
  users$: Observable<User[]> = this.usersService.getUsers();
  constructor(
    private itemsService: ItemsService,
    private usersService: UsersService
  ) { }
}

We can use the items$ observable to bind directly to our template with the async pipe as we itereate over the collection to display each individual item.

<md-list-item *ngFor="let item of items$ | async">
  <app-item [item]="item"></app-item>
</md-list-item>

We will bind to the users$ observable in a similar fashion as we populate the options within a select element. We are populating the newUser.userId property by binding to [value]=”user.id” on our option elements.

<md-list-item *ngIf="shouldShowNewItem">
  <md-input md-line placeholder="Name" [(ngModel)]="newItem.name"></md-input>

<div class="form-footer" md-line>
<div class="select-wrapper">
      <select [(ngModel)]="newItem.userId"><option value="undefined">Select...</option><option *ngFor="let user of users$ | async" [value]="user.id">{{user.name}}</option></select>
    </div>
    <button md-raised-button color="accent" (click)="addItem()">Add</button>
  </div>
</md-list-item>

From here on out, we are exposing exactly the same functionality as we did with within the UsersComponent class. We create new items by calling addItem which calls itemsService.addItem and then clears the decks by calling itemsService.initializeNewItem.

export class ItemsComponent {
  items$: Observable<Item[]> = this.itemsService.getItems();
  users$: Observable<User[]> = this.usersService.getUsers();
  newItem: Item = this.itemsService.initializeNewItem();
  constructor(
    private itemsService: ItemsService,
    private usersService: UsersService
  ) { }
  addItem(): void {
    this.itemsService.addItem(this.newItem);
    this.newItem = this.itemsService.initializeNewItem();
  }
}

And then we have the toggleNewItem which controls the visibility of the UI for creating a new item.

export class ItemsComponent {
  items$: Observable<Item[]> = this.itemsService.getItems();
  users$: Observable<User[]> = this.usersService.getUsers();
  shouldShowNewItem: boolean = false;
  newItem: Item = this.itemsService.initializeNewItem();
  constructor(
    private itemsService: ItemsService,
    private usersService: UsersService
  ) { }
  toggleNewItem(): void {
    this.shouldShowNewItem = !this.shouldShowNewItem;
    if (!this.shouldShowNewItem)
      this.newItem = this.itemsService.initializeNewItem();
  }
  addItem(): void {
    this.itemsService.addItem(this.newItem);
    this.newItem = this.itemsService.initializeNewItem();
  }
}

Users Items

users-items-feature

And now for the best part of this lesson! How do we display a list of users with their respective items? We have two distinct application models connected only by an id.

users-items-feature

User Items Model

Let’s start out by defining a new model that is the combination of the two. We are going to create a UserItems interface that has an id and name property. This part is basically the User object portion of the equation. We are going to add in an additional items property that is typed to be a collection of Item objects. We are essentially saying “here is a user and it has many items”.

// shared/user-items.model.ts

import { Item } from './items';

export interface UserItems {
  id: string,
  name: string
  items: Item[]
}

Users Items Service

Here is my favorite part! Because our application store exposes users and items as observables, we can use Observable.combineLatest to create a NEW observable that is in exactly the shape that we want.

Let us step through the getUsersItems method and see how this works. First, notice that the getUsersItems returns an observable that contains a collection of UserItems objects. This is important as it exactly mirrors how we are consuming our other collections. Because we want to combine the data from users and items, we will create a reference to them both by calling store.select and storing the returned observables as users$ and items$ respectively.

// shared/users-items.service.ts

export class UsersItemsService {
  constructor(private store: Store<AppStore>) { }
  getUsersItems(): Observable<UserItems[]> {
    const users$: Observable<User[]> = this.store.select('users');
    const items$: Observable<Item[]> = this.store.select('items');
    return Observable.combineLatest(users$, items$, (users, items) => {
      return users.map(user => Object.assign({}, user, {
        items: items.filter(item => item.userId === user.id)
      }));
    });
  }
}

With our two main observables in our possession, we will call Observable.combineLatest to produce our final combined observable. How this works is that Observable.combineLatest takes a list of observable sequences as its arguments, with the last argument being a function that we use to perform some logic on the previous arguments. In our case, we are passing in users$ and items$ which then gets passed into the function argument as users and items parameters. Within this function, we are mapping over users and returning a new object that contains all of the properties of the user object with an additional items property. The items property is a filtered list of items that we calculate by comparing item.userId against user.id.

Whenever any input Observable emits a value, it computes a formula using the latest values from all the inputs, then emits the output of that formula.

Users Items Component

Outside of the UsersItemsService, the rest of the application is oblivious to the fact that they are dealing with a computed observable and not a collection that lives within the application store. This.is.awesome!

Our UsersItemsComponent retrieves its collection just like the other two components. It calls a service that returns an observable that contains the collection it needs which in this case gets assigned to usersItems$.

//users-items/users-items.component.ts

export class UsersItemsComponent {
  usersItems$: Observable<UserItems[]> = this.usersItemsService.getUsersItems();
  constructor(private usersItemsService: UsersItemsService) { }
}

Which we then consume within our template using the async pipe as we stamp out UserItems components with ngFor.

<md-list-item *ngFor="let userItems of usersItems$ | async">
  <app-user-items [userItems]="userItems"></app-user-items>
</md-list-item>

The UserItemsComponent has a single input of userItems which we can then use in our template to lay out the individual items.

// users-items/user-items/user-items.component.ts

export class UserItemsComponent {
  @Input() userItems: UserItems;
}

We are now in the territory of standard Angular 2 this far down the stream.


<div md-line>{{userItems.name}}</div>
<md-card-subtitle md-line *ngFor="let item of userItems.items">{{item.name}}</md-card-subtitle>

Review

Let’s just do a quick review of the main technique that we covered in this lesson. When working with redux, I like to abstract all interactions with the store into services. I put a service in front of the store to handle asynchronous operations and a service behind the store to handle computed observables. There are a lot of mechanisms and libraries that are available within the redux ecosystem to handle complex behavior but I have found the majority of the things that I run into can be handled in plain old services. This is, in part, possible because we are leveraging the power of observables which just happen to be very good at sequencing asynchronous operations and dynamically calculating values as they become available.

The Users and Items features are a standard redux implementation with the really interesting part being how to combine and display the two underlying models. We were able to do this with Observable.combineLatest which takes multiple streams and returns a new observable stream. When a new value is emitted from either of the origin streams, a new computed value is produced and emitted on the stream that was created by calling Observable.combineLatest. This is particularly awesome because we are essentially producing computed state on the fly that does not exist anywhere in the application in that form. Equally impressive is that outside of the service, the rest of the application is oblivious as to the nature of the data. It just knows that it is getting an observable and it can handle things from there.

In closing, working with related models was probably one of the hardest things for me to wrap my mind around when working with redux. Once I learned how to combine my models using Observable.combineLatest, it was like the final piece in the track clicked into place and I had a clear picture of data and events flowed through the application in an almost circular pattern. State moves “down” through the application on one-half of the track while events move “up” through the application on the other side. And where events hand off the baton to state, there is a service to handle asynchronous events and where state hands of the baton to events, there is a service to handle computed state.

Resources

Build a Better Angular 2 Application with Redux and ngrx

@ngrx/store Documentation

Observable.combineLatest Documentation

Observable.combineLatest Marbles

Leave a Comment