In the first and second part of this small series, we have discussed implementations that have flaws and can introduce side-effects to our data flow. We have learned how combining our streams and modeling our our data flow to yield a single result helped with solving the introduced problems.

In this third part of the series we want to talk about another advantage when modeling our streams in this manner, specifically in the context of an Angular application.

Composing data streams to yield a single result and leveraging the async pipe

Similar to the previous posts, we want to combine streams to retrieve from multiple entities. This time however, we make sure that the final product of our stream is our view model.

For this example we keep it rather simple and combine the request for a user object with the request for a list of roles in our system, so we can fill the view with a nicer representation than just a role id.

// Stream that emits an user object when the userId route param changes
const user$ = this.route.params.pipe(
  switchMap(({ userId }) => requestUser(userId)),
);

// Stream that emits a list of all role objects once
const roles$ = requestRoles();

// Combine both streams
const userAndRoles$ = combineLatest(user$, roles$);

// Transform combined result into UserView model
this.userView$ = userAndRoles$.pipe(
  // Create a UserView object containing the user's properties and its roles
  map(([user, roles]) => ({
    ...user,
    roles: resolveUserRoles(user, roles)
  }))
);

In the implementation above, we create two streams. One to request and emit a new user object when the userId route param changes, and one to emit a list of all roles once (technically, we could emit live updates of roles, too). We use the combineLatest operator which combines the latest emit of the given streams into a single emit. In our case this serves two purposes. First, once the user$ emits again, which happens when we change the route accordingly, the same pipeline runs and the view will be updated with the new user and its respective roles. Additionally, we cannot display the view until the roles are received. combineLatest will hold back on its emit, until both streams have produced a value.

Finally, we can now use the userView$ stream in our template and subscribe to it via the async pipe. This way we do not even have to handle subscriptions. Another advantage of using the async pipe is that we can easily switch our change detection strategy to OnPush as the template will only need to be updated when our stream emits.

<dl *ngIf="userView$ | async as userView">
  ...
</dl>

You can see this live on stackblitz.

Conclusion

In this series so far, we briefly discussed how combining our streams to yield a single result helps us eradicate potential side-effects. We now illustrated how following this principle can make working with streams in Angular applications easier, as the async pipe implicitly handles subscriptions for us. Ultimately, this comes with performance improvements, as we can make use of the OnPush change detection strategy.

Related Articles