Declarative, Reactive, Data and Action Streams in Angular

Post Editor

We can use action and data streams declaratively to react to user actions. Leveraging the power of RxJs operators we can transform our data and allow our template to subscribe to the Observable using the Async pipe.

6 min read
post

Declarative, Reactive, Data and Action Streams in Angular

We can use action and data streams declaratively to react to user actions. Leveraging the power of RxJs operators we can transform our data and allow our template to subscribe to the Observable using the Async pipe.

post
post
6 min read
6 min read

That title has a lot of big scary wordsLink to this section

I get it: if you're new to Angular that sentence could be overwhelming. What does reactive mean? What is declarative? Ok, data fetching, I get, and I know about the HTTP Client, but what are you talking about? Well, first let's nail down some key terms, some basic fundamentals, and then let's iterate from there. In this article, I hope to take you to the beginning of the journey I'm still going through. Let's get started!

HTTP ClientLink to this section

There are some basic assumptions that I'm making here. First, I assume you know Angular basics such as components and the HTTP Client. Second, this isn't going to be an in-depth post on RxJs that would require a series of posts. But I do assume some basic knowledge of things like Observables, Subjects, and subscribing.

So typically in Angular applications, you'll need to get some data from a server and display it for your users to read and interact with. Most of the time we'll use methods like,

Following good practices, this method lives in our service and then is called in our component when the component is initialised.

Then with the user’s property in our class, we can use structural directives in our template and display our UI:

<>Copy
getUsers() { return this.http.get<Users[]>(`${this._rootUrl}/users`) .pipe(catchError(err=> ( this.handleError(err) )) }

Following good practices, this method lives in our service and then is called in our component when the component is initialized.

<>Copy
ngOnInit(): void { //dont forget to unsubscribe! this.subcription = this.userService.getUsers() .subscribe(res => this.users = res) }

Then with the users property in our class, we can use structural directives in our template and display our UI.

<>Copy
<div *ngFor="let user of users">{{user.name}}</div>

This is a completely valid way, but is there a better way?Link to this section

This is the question I started asking myself. There are a few improvements that could be made. I can think of one specifically by using the Async Pipe to subscribe to the observable so I don't have to manage my own subscriptions.

That in itself is a huge win and makes this a technique that a lot of people use. But I wasn't satisfied and I ran into this problem at work that involved taking data from two different APIs. Using a procedure like a pattern didn't feel like the solution to me.  Then I saw a talk from Deborah Kurata and was hungry to learn more. Enter in the Declarative and Reactive approach.

Key TermsLink to this section

First, let's talk about what reactive and declarative mean. For starters, we may recognise the imperative or procedure like way of coding. This is where you describe each and every step of what you want to accomplish.

For instance, say you want to navigate to a particular file in your filesystem that's nestled deep in a folder structure. You don't remember what's all in there and you can only use the terminal. You're going to be doing a lot of ls and cd until you get where you need to go. Those are procedures. You're describing exactly each step until you get to where you wanna go.

Now, what would a declarative approach look like? Simply saying whatever-the-file-is and then the machine figures out the rest by itself. This is nice if the machine knows how to do it but most of the time it does not and we need to describe what we want to happen and all the steps to achieve that. Now reactive programming is a bit harder to explain and I think I'll punt to this article The introduction to Reactive Programming you've been missing.

But the short of it is:

<>Copy
//C REACTS to changes in a or b let a = 1; let b = 2; let c = a + b; //3 //at this point in time b = 3; // C will now be 4. It recomputed its value based on changes to the things that make up its value.

Declarative Data StreamsLink to this section

Now that we know where we have been, let's talk about where we are going. Let's declare a data stream:

<>Copy
allUsers$ = this.http.get<User[]>(`${this._baseUrl}/users`).pipe( map(users => users.map( user => ({ ...user, username: `${user.username}@${user.website}` } as User) ) ), catchError(err => { console.error(err); return throwError(err); }), shareReplay(1) );

So let's do a breakdown of this code. In our service, we are declaring a property in our class called allUsers$. Now the $ at the end is a community convention to let you know that this is an Observable stream. Leveraging RxJs operators we could do any kind of data transformation that we want but in this case, I'm using the map operator to receive that User[] value, Array.map() over every User object and then return a new object with a new property called username.  Then we cast that return value back as User for good measure in case our typings got messed up and TS couldn't infer the type.

Next, we do some error handling with catchError. Now typically you'd log it using a Logger service and keep the logs on a server somewhere but for now, we will just error in the console. I do this here so if there was a problem we could see in the service where it was and then we return throwError so that it propagates up to the object that subscribes to it (our component) and they can handle it there.

Lastly, we shareReplay(1). This allows us to cache and reuse the data we've already got if someone subscribes to the Observable later on. For instance, say we have two components that subscribe to the Observable at different times. The first subscriber will kick off the HTTP request. Since we are sharing and replaying the result, the next subscriber gets the value the first one did and another HTTP call does not have to be made.

How to use Data streamsLink to this section

Using a data stream is incredibly simple! Here is a recipe for using the stream,

  1. Inject the service into the target component using private userService: UsersService
  2. Capture a reference of the data stream from your service. For example:
<>Copy
this.users$ = this.userService.allUsers$.pipe( //Our error thrown from the service bubbles to the component where we handle //it. I'm just simply setting a property to true //You have to return an Observable so I just return a empty observable that completes catchError(err => { this.error = true; return EMPTY; }) );

3. Now we subscribe from our template with the Async pipe!

<>Copy
<ng-container *ngIf="users$ | async as users">

Reactive Data Streams with Action StreamsLink to this section

Some of the times our data in our applications is read-only.  This makes things easy for us because we just subscribe to the Observable and display the data in the template and call it a day.  

Other times we want our users to have the ability to modify the data or take actions on that data. We can call these user actions Action Streams.  We can create action streams using RxJS Subjects and throwing values into those streams. Taking an action stream we can have our data stream react to those actions and leverage RxJs operators to modify our data.  Here's an example of declaring an action stream that emits a selectedUser.

<>Copy
private selectedUserSubject = new BehaviorSubject<number>(null); selectedUserAction$ = this.selectedUserSubject.asObservable(); onSelectedUser(id) { this.selectedUserSubject.next(id); }

To break this down we have our BehaviorSubject that emits numbers and its Observable counterpart. We then have a helper method that when called from the component emits the ID of the selected user into that action stream. Using this information combined with the allUser$data stream we can create a stream that emits the selected user and reacts to the user's action:

<>Copy
selectedUserData$: Observable<User> = combineLatest([ this.allUser$, this.selectedUserAction$ ]).pipe( map(([allUsers, selectedUser]) => allUsers.find(u => u.id === selectedUser)) );

We set the selectedUserData$ property to be the result of the combineLatest operator. What this does is takes the last value emitted from both streams and return those values as an array. Using array destructuring in the map operator we return the result of the allUsers array find function. So now every time we emit a new ID into the action stream this pipe runs returning us a new user.

That's all folks!Link to this section

Thanks for sticking around! Using a technique like this offers a lot of flexibility and introduces you to reactive, push based architectures. Using these architectures you'll notice your UI feels more fluid! Here's a StackBlitz.

Discuss with community

Share

About the author

author_image

Tired father of three wonderful children. Started in a tire shop and found my way into tech. Trying to solve school safety problems.

author_image

About the author

Chandler Baskins

Tired father of three wonderful children. Started in a tire shop and found my way into tech. Trying to solve school safety problems.

About the author

author_image

Tired father of three wonderful children. Started in a tire shop and found my way into tech. Trying to solve school safety problems.

NxAngularCli
NxAngularCli
NxAngularCli

Featured articles

Angularpost
10 May 20219 min read
Angular Forms: reactive design patterns catalog

In this post, you'll find a set of design patterns for building Angular forms based on two pillars: separation of responsibilities and reactive programming to tackle the complexity of rich and complex Angular forms.

Angularpost
6 May 20216 min read
A journey into NgRx Selectors

This article dives deep into NgRx selectors and will help you understand what role that play in NgRx architecture and how they help decrease the complexity of a codebase