RxJS in Angular: Part I

Anyone building Angular applications should have at least a working knowledge of RxJS. Angular itself is constructed using RxJS and built around its core concepts. But there is more to it than that. In practice, we can leverage RxJS and observable streams to craft cleaner, more maintainable code while simultaneously cutting down on the lines we write (which directly impacts bundle size).

Let's get started.

Leveraging RxJS to Minimize Component State

At its heart, Angular is all about the state of components—and the app as a whole—and how that state gets rendered to the user interface. There are numerous situations where representing transient data as streams within the view proves invaluable. This approach is particularly useful when dealing with forms and other data that changes frequently.

Here's a look at how we often get things wrong:

  1. We approach a new task by first thinking about how the state will evolve.
  2. We introduce **new** pieces of state into our component (adding properties, nested objects, and so on).
  3. We create new methods to encapsulate the logic for modifying this newfound state.
  4. We then write complicated logic directly inside our templates.

Let's illustrate this with a concrete example:

import { Component } from '@angular/core';

@Component({
  selector: 'my-component',
  template: `
    <select [(ngModel)]="selectedUserId" (ngModelChange)="changeUser()">
      <option>Select a User</option>
      <option *ngFor="let user of users" [value]="user.id">{{ user.name }}</option>
    </select>
    <select [(ngModel)]="blackListedUsers" (ngModelChange)="changeUser()" multiple>
      <option *ngFor="let user of users" [value]="user.id">{{ user.name }}</option>
    </select>
    Allow black listed users <input type="checkbox" [(ngModel)]="allowBlackListedUsers"/>
    <button [disabled]="isUserBlackListed && !allowBlackListedUsers">Submit</button>
  `,
})
export class MyComponent  {
  users = [
    {name: 'John', id: 1},
    {name: 'Andrew', id: 2},
    {name: 'Anna', id: 3},
    {name: 'Iris', id: 4},
  ];

  blackListedUsers = [];

  selectedUserId = null;
  isUserBlackListed = false;
  allowBlackListedUsers = false;

  changeUser() {
    this.isUserBlackListed = !!this.blackListedUsers.find(
      blackListedUserId => +this.selectedUserId === blackListedUserId
    );
  }
}

Consider a page with a select dropdown for choosing a user (populated from an array using *ngFor). A second dropdown lists the same users, but selecting someone here marks them as **blacklisted**, preventing them from being submitted. If a user is selected from the first dropdown who happens to be blacklisted, the Submit button should become disabled. There's one more wrinkle: an "allow blacklisted users" checkbox. When checked, it permits submitting a blacklisted user, meaning the button stays enabled even if a blacklisted user is chosen. Let's attempt this without RxJS, using straightforward template-driven form models:

We end up with two arrays, three form bindings, and a separate method invoked on (ngModelChange) to manage state changes on the fly. This perfectly illustrates the classic four-step (mis)thinking I outlined above, which pushes us toward writing more tangled code.

  1. We identify a need for state (isUserBlackListed and allowBlackListedUsers) to hold data that's truly only needed for the template;
  2. We add these state properties to our component and bind them with [(ngModel)];
  3. We create a method (changeUser) to manipulate that state;
  4. We also embed logic directly in the template ([disabled]="isUserBlackListed && !allowBlackListedUsers").

Why is this approach problematic to begin with? For one, it makes tracing the application's logic more challenging. If I'm reviewing this code and notice a button becoming disabled intermittently, I'd have to follow these steps:

  1. Locate the [disabled] binding and see that it references two properties, isUserBlackListed and allowBlackListedUsers;
  2. Check the component code to see they are just basic properties initially set to false;
  3. Scan the component.ts file for all references to them. This seems straightforward, but what if these properties are used across multiple methods? I'd need to carefully inspect each one to pinpoint exactly which is responsible for the button's disabled state;
  4. Parse and understand the method I finally locate. In this toy example, it's easy; in a real-world scenario, that logic could be quite complex.

A second issue is that whenever we encounter another piece of similar logic, we'll end up multiplying the number of properties and methods that modify them in our component.

So, what's the better approach?

Embracing a Reactive Mindset

Let's now outline a simple three-stage plan. Approaching the same problem from a different angle—this time using Reactive Forms and RxJS—we'll follow these steps:

  1. Determine which aspects of the state impact the UI and transform them into Observable streams;
  2. Apply RxJS operators to process these streams and derive the final state relevant for the UI;
  3. Use the async pipe to bring the computed result directly into the template.

Here's how the revised implementation looks:

import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { combineLatest } from 'rxjs';
import { map, startWith } from 'rxjs/operators';

@Component({
  selector: 'my-app',
  template: `
    <select [formControl]="selectedUserId">
      <option>Select a User</option>
      <option *ngFor="let user of users" [value]="user.id">{{ user.name }}</option>
    </select>
    <select [formControl]="blackListedUsers" multiple>
      <option *ngFor="let user of users" [value]="user.id">{{ user.name }}</option>
    </select>
    Allow black listed users <input type="checkbox" [formControl]="allowBlackListedUsers"/>
    <button [disabled]="isDisabled$ | async">Submit</button>
  `,
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  users = [
    {name: 'John', id: 1},
    {name: 'Andrew', id: 2},
    {name: 'Anna', id: 3},
    {name: 'Iris', id: 4},
  ];

  blackListedUsers = new FormControl([]);
  selectedUserId = new FormControl(null);
  allowBlackListedUsers = new FormControl(false);
  isDisabled$ = combineLatest([
    this.allowBlackListedUsers.valueChanges.pipe(startWith(false)),
    this.blackListedUsers.valueChanges.pipe(startWith([])),
    this.selectedUserId.valueChanges.pipe(startWith(null), map(id => +id)),
  ]).pipe(
    map(
      ([allowBlackListed, blackList, selected]) => !allowBlackListed && blackList.includes(selected),
    ),
  )
}

As you can see, we now have a property that is an Observable, used to manage a portion of the UI. It uses combineLatest to merge the outputs from our three form controls, then applies logic to derive a boolean state from their combined values.

Note: we incorporated startWith because in Angular, formControl.valueChanges won't emit right away—it only triggers after the user changes the control via the UI or programmatically via setValue. Since combineLatest only fires when **all** source Observables have emitted at least once, we force each one to immediately emit its default value.

Now, if I revisit this component's template and wonder **"what disables this button?"**, my steps are:

  1. Spot the [disabled]="isDisabled$ | async" binding; the trailing dollar sign immediately signals it's an Observable;
  2. Jump to that property's definition to observe it's a combination of three data sources;
  3. Trace how that data is transformed into a boolean.

That's the entire process. The isDisabled$ Observable isn't referenced anywhere else on the component. Even if it were subscribed to elsewhere, it doesn't matter—others can subscribe, but they cannot alter its data. If there's a bug causing the button to be wrongly disabled (or enabled), we can be certain the issue lies solely in the definition of isDisabled$ and its operators, and nowhere else.

So, this change made our code:

  1. Easier to navigate and search;
  2. Cleaner; related pieces of logic are consolidated into a single location instead of spread throughout the component;
  3. Declarative rather than imperative.

And all of this shares a single guiding principle:

Properties are easier to reason about than methods

That's a great start. What are some other examples where RxJS can enhance our Angular code?

Managing Toggling State

There are countless scenarios where two pieces of data are interdependent—one can modify the other, and the relationship often reverses. Here's a classic real-world example: a component with a search button. Clicking it reveals a search input beside it. Clicking anywhere else hides that input, unless it's non-empty—reminiscent of the search field Medium uses. We're going to build something similar.

Just as before, let's first tackle it without RxJS. Here's the implementation:

@Component({
  selector: 'my-app',
  template: `
    <button (click)="showSearchInput($event)">Search</button>
    <input [(ngModel)]="query" *ngIf="isSearchInputVisible"/>
  `,
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  query = '';
  isSearchInputVisible = false;

  showSearchInput(event: MouseEvent) {
    event.stopPropagation();
    this.isSearchInputVisible = true;
  }

  @HostListener('document:click')
  hideSearchInput() {
    if (this.query === '') {
      this.isSearchInputVisible = false;
    }
  }
}

Here's what it accomplishes: we maintain a boolean state called isSearchInputVisible, toggle it via two separate methods—one triggered on button click, the other on any document click. We also call stopPropagation on the button's click to prevent it from bubbling up and being mistaken for a document click—the button is meant to open the search, not close it! It's a basic implementation, but it's what we'd typically write without RxJS. This approach embodies the four incorrect steps I described earlier in the article. Now, let's refactor it using RxJS:

@Component({
  selector: 'my-app',
  template: `
    <button #btn>Search</button>
    <input [(ngModel)]="query" *ngIf="isSearchInputVisible$ | async"/>
  `,
  styleUrls: [ './app.component.css' ]
})
export class AppComponent implements AfterViewInit {
  @ViewChild('btn', {static: true}) buttonRef: ElementRef<HTMLButtonElement>;
  query = '';
  isSearchInputVisible$: Observable<boolean> = of(false);

  ngAfterViewInit() {
    this.isSearchInputVisible$ = merge(
      fromEvent(this.buttonRef.nativeElement, 'click').pipe(tap(e => e.stopPropagation()), mapTo(true)),
      fromEvent(document.body, 'click').pipe(filter(() => this.query === ''), mapTo(false))
    ).pipe(startWith(false));
  }

}

Here's the refactored version: We have one definitive source of truth, zero methods, and the complete logic is embedded in the operators applied to the source observables. Let's break down what we did:

  1. We started with two streams—clicks on the button and clicks on the entire document;
  2. For the first stream (button clicks), we called stopPropagation to prevent interference with the document stream, then mapped those events to the value true;
  3. The second stream (document clicks) is simply mapped to the value false, but only when the query field is empty (thus the filter operator);
  4. The merge of these two streams yields exactly the behavior we need—button clicks open the search, and all other clicks close it!

You might be wondering…

  1. Why did we put this within ngAfterViewInit? Because buttonRef isn't accessible until the view has been rendered. We wait until that point to begin capturing events from it;
  2. Why is .pipe(startsWith(false)) needed after the merged Observable? Without it, the Observable's value would flip from undefined to false instantly, which triggers an ExpressionChangedAfterItHasBeenCheckedError.

Haven't we overlooked something?

One might think we should manually unsubscribe from the Observable, but that's unnecessary—the async pipe manages that automatically for us.

Yet again, shifting our code from imperative to declarative style with RxJS has significantly improved its quality.

Summary

RxJS is an incredibly powerful library—no wonder a framework as extensive as Angular is built around it. It offers a vast array of concepts and techniques that can be used to improve the quality, readability, maintainability, and clarity of our Angular code. By no means is it limited to the examples discussed here—I look forward to exploring more RxJS use cases within Angular in future articles.