Imperative vs Declarative Programming with RxJS: A Search Filter

15 min read

Originally published July 20, 2021

Do you know what the difference between declarative and imperative programming is? If you are using RxJS/Observables which allow for reactive programming (a type of declarative programming), then understanding this difference could lead to huge improvements in your applications.

If you are only programming imperatively (which you could say is the default way to program), and you are making use of RxJS/Observables, then you are kind of going against the grain and might be making things harder for yourself.

Programming reactively/declaratively feels more unnatural and complex initially, but once it starts to click, it feels like magic.

Outline

Source code

Imperative vs Declarative: A Brief Introduction

You might want to spend some time researching the concepts of imperative programming and declarative programming to gain a better understanding, but I will try to give a brief overview. Here is how I like to think of it (specifically in relation to RxJS):

An imperative is an order: this is what you will do. If you are programming imperatively, you are telling your code how to work step by step: Get this data, then do this, then do that.

A declaration is a statement: this is what I want. If you are programming declaratively, you are telling your code what you want. We set up pure functions (same inputs = same outputs) for our data sources to flow through which describe what we want. As our data sources change, our application will automatically react to these changes.

These descriptions are a bit fuzzy, and hard to understand intuitively (or maybe I'm just not good enough at explaining it!). Let me try to paint a bit more of a picture here.

One thing that really helped me understand the difference between imperative and declarative programming with RxJS is (and I regret that I can't remember who I actually heard this from, but let me paraphrase):

With RxJS, as soon as you subscribe to something to pull data out of a stream, you are coding imperatively

Simply using RxJS/Observables does not make your code reactive/declarative.

The flow of a stream, and merging streams together into new streams that create new results, is the key concept behind reactive/declarative programming with RxJS. Once you subscribe you are manually pulling data out of the stream, rather than composing streams together to get the results you want automatically. This act of composing/combining streams together is where we are describing what we want to be done with all the data passing through the stream.

You might imagine a factory line with conveyor belts and robotic arms picking parts and moving them around, like in Factorio. With this analogy, you can think of the conveyor belts as the observable streams, the items on those belts as the data being emitted from the streams, and the various robotic arms and other contraptions as the RxJS operators that combine/modify those streams:

Everything works automatically as defined by the pure functions the conveyor belts and robotic arms are governed by. Subscribing is like you coming along and taking something off of the conveyor belt. Now you have the item, and you have to manually decide what to do with it in an imperative way.

This doesn't mean you can't ever subscribe to an Observable - in fact, that's necessary in some manner in order for the observable to be triggered in the first place - but try to ask yourself if you really need to subscribe here. Can you instead compose multiple streams together, or use RxJS operators, to achieve what you need without subscribing?

An Imperative Search Filter

Hopefully the above has got your brain ticking away on some concepts a little bit, but the chances are that all of this probably seems very blurry and confusing if you don't already have an understanding of these concepts.

To help make things clearer, let's walk through an example. We are going to build a feature: a search box that can filter a list of items. We will first build it imperatively and then we will build the exact same thing again with a declarative approach.

The tricky thing here is that we will be using observables in both cases. This should help highlight the difference between programming imperatively with observables, and programming declaratively/reactively with observables. This should also highlight the benefit of the declarative approach.

NOTE: The code we are about to look at is imperative. I am showing this as an example of what NOT to do if you want to code declaratively.

For both of our approaches, we are going to have a ClientService that looks like this:

import { Injectable } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs';

import { Client } from '../interfaces/client';

@Injectable({
  providedIn: 'root',
})
export class ClientService {
  private clients$ = new BehaviorSubject<Client[]>([
    { firstName: 'Arya', lastName: 'Stark' },
    { firstName: 'Robb', lastName: 'Stark' },
    { firstName: 'Olenna', lastName: 'Tyrell' },
    { firstName: 'Tyrion', lastName: 'Lannister' },
    { firstName: 'Davos', lastName: 'Seaworth' },
    { firstName: 'Renly', lastName: 'Baratheon' },
    { firstName: 'Stannis', lastName: 'Baratheon' },
    { firstName: 'Roose', lastName: 'Bolton' },
  ]);

  constructor() {}

  public getClients(): Observable<Client[]> {
    return this.clients$;
  }
}

NOTE: It is not necessary to use .asObservable here when returning this.clients$ to prevent other parts of the code calling the next method on the BehaviorSubject. Since we give the method a return type of Observable<Client[]>, the next method will be invalid anyway.

All it does is allow us to grab the data we need for our list as an Observable. We will also have a template for the page the search is displayed on that looks like this (the declarative template will look slightly different):

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>Imperative</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-searchbar [formControl]="searchField" debounce="100"></ion-searchbar>
  <ion-list>
    <ion-item *ngFor="let client of filteredClients">
      {{client.firstName}} {{client.lastName}}
    </ion-item>
  </ion-list>
</ion-content>

We have a search bar that is linked to a FormControl that we will use to get search values. Then we just have a list that displays the list of filteredClients (i.e. any clients that match the current search term).

Now let's look at our imperative implementation of the logic to get this search field working:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { FormControl } from '@angular/forms';
import { ClientService } from '../services/client.service';
import { Client } from '../interfaces/client';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({
  selector: 'app-imperative',
  templateUrl: './imperative.page.html',
  styleUrls: ['./imperative.page.scss'],
})
export class ImperativePage implements OnInit, OnDestroy {
  public searchField: FormControl;
  public clients: Client[];
  public filteredClients: Client[];

  // Trigger this to unsubscribe observables
  private destroy$: Subject<boolean> = new Subject<boolean>();

  constructor(private clientService: ClientService) {
    this.searchField = new FormControl('');
  }

  ngOnInit() {
    // Get our client data
    this.clientService
      .getClients()
      .pipe(takeUntil(this.destroy$))
      .subscribe((clients) => {
        this.clients = clients;

        // Set list to all clients by default
        this.filteredClients = clients;
      });

    // React to changes in the search term
    this.searchField.valueChanges
      .pipe(takeUntil(this.destroy$))
      .subscribe((searchTerm) => {
        this.filteredClients = this.clients.filter(
          (client) =>
            searchTerm === '' ||
            client.firstName.toLowerCase().includes(searchTerm.toLowerCase()) ||
            client.lastName.toLowerCase().includes(searchTerm.toLowerCase())
        );
      });
  }

  ngOnDestroy() {
    this.destroy$.next(true);
    this.destroy$.unsubscribe();
  }
}

Let's break down what is happening here:

  1. We set up a form control for the search field
  2. We get the data for our clients list by subscribing to the Observable from the ClientService
  3. We subscribe to the valueChanges observable for the search field
  4. Whenever we receive a new value, we update our filteredClients data that the list uses
  5. When the component is destroyed, we fire the destroy$ observable to clean up our subscriptions

This all works fine, and is a reasonably common way to go about doing something like this. I would say this is probably how most people would approach building something like this within an Angular application. It's kind of the default/obvious way to do it, which is generally the case for the imperative approach.

This approach is fine in general, but it can get a bit awkward. What if our ClientService emits a new set of clients? With the code we have currently, this is going to overwrite any filter that is active until the valueChanges observable is triggered again by entering a new searchTerm. This can still be handled imperatively of course, but it's just a bit more awkward when we are relying on observables like this, but not really programming in a reactive/declarative way.

A Declarative Search Filter

Now let's look at the exact same functionality implemented in a declarative way. The ClientService remains unchanged:

import { Injectable } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs';

import { Client } from '../interfaces/client';

@Injectable({
  providedIn: 'root',
})
export class ClientService {
  private clients$ = new BehaviorSubject<Client[]>([
    { firstName: 'Arya', lastName: 'Stark' },
    { firstName: 'Robb', lastName: 'Stark' },
    { firstName: 'Olenna', lastName: 'Tyrell' },
    { firstName: 'Tyrion', lastName: 'Lannister' },
    { firstName: 'Davos', lastName: 'Seaworth' },
    { firstName: 'Renly', lastName: 'Baratheon' },
    { firstName: 'Stannis', lastName: 'Baratheon' },
    { firstName: 'Roose', lastName: 'Bolton' },
  ]);

  constructor() {}

  public getClients(): Observable<Client[]> {
    return this.clients$;
  }
}

The template is more or less the same, but there is a slight difference:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>Declarative</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-searchbar [formControl]="searchField" debounce="100"></ion-searchbar>
  <ion-list>
    <ion-item *ngFor="let client of filteredClients$ | async">
      {{client.firstName}} {{client.lastName}}
    </ion-item>
  </ion-list>
</ion-content>

Now we are using an observable of filteredClients$ to supply the data to our list, and we are using the | async pipe to handle automatically subscribing to and unsubscribing from this observable for us. With the imperative approach, the data being supplied to the list was just a normal array.

The biggest change is in the logic for the component:

import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { combineLatest, Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { ClientService } from '../services/client.service';
import { Client } from '../interfaces/client';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements OnInit {
  public searchField: FormControl;
  public filteredClients$: Observable<Client[]>;

  constructor(private clientService: ClientService) {
    this.searchField = new FormControl('');
  }

  ngOnInit() {
    // Get a stream of our client data
    const clients$ = this.clientService.getClients();

    // Get a stream of our search term
    const searchTerm$ = this.searchField.valueChanges.pipe(
      startWith(this.searchField.value)
    );

    // Combine the latest values from both streams into one stream
    this.filteredClients$ = combineLatest([clients$, searchTerm$]).pipe(
      map(([clients, searchTerm]) =>
        clients.filter(
          (client) =>
            searchTerm === '' ||
            client.firstName.toLowerCase().includes(searchTerm.toLowerCase()) ||
            client.lastName.toLowerCase().includes(searchTerm.toLowerCase())
        )
      )
    );
  }
}

Let's again break down what is happening here:

  1. We set up a form control for the search field
  2. We obtain an observable stream of clients
  3. We obtain an observable stream of the search term
  4. We combine both of those streams in a new stream of filtered clients

Instead of getting the data we need from both streams individually, we combine the clients$ and searchTerm$ streams into a single new stream called filteredClients$. We don't take the data from these streams by subscribing and manually do something with it, we just describe how data from these streams should be combined.

You can think of this kind of like making a soup. If we want to make a list of filtered clients soup, then the ingredients we need for that soup is the clients and what we want to filter them by. We obtain an observable stream of both of those ingredients, and then we mix them together using combineLatest.

The combineLatest operator will combine both of the streams, and it will give us the latest value from each of those streams to work with. The combineLatest operator will not be triggered until all of the streams being supplied to it have emitted at least one value, which is why we use the startWith operator on our valueChanges observable. The valueChanges observable will only emit after the search field has its value changed, but we still need an initial set of data to display.

Since we have both of the values from clients$ and searchTerm$ available in our new stream, we can use a map to change that stream into whatever we need. In this case, we want to modify the stream so that rather than just giving us the latest clients and searchTerm values it will give us an array of clients filtered by that searchTerm.

The resulting filteredClients$ stream will emit values that are an array of clients filtered by the search term. Any time either the clients$ or the searchTerm$ streams emit new values, the filteredClients$ stream will automatically emit a new value, which is then displayed in our template. Thanks to the | async pipe, we don't need to subscribe to anything manually and there is no need for us to clean up any subscriptions when the component is destroyed.

Summary

I think most people would agree that the declarative approach is not as intuitive as the imperative approach. However, even if you aren't familiar with reactive programming, I think it is also quite clear to see that despite the added difficulty the declarative approach is actually quite a bit simpler/neater.

The general idea is that instead of using subscribe, we try to use the various RxJS operators merge/combine/switch/manipulate streams to do that we need. Unfortunately, combineLatest is just scratching the surface.

There are a lot of RxJS operators, which is both good and bad. Good because you can do just about anything with them, bad because it can be terribly confusing learning when to use what and when. Thinking in terms of how to solve problems with observable streams can be quite challenging.

Although there are a lot of operators to learn, there are a few there are used far more often than the rest. If you can learn these ones, then you can probably solve most problems you will face:

To help you decide which operator might suit your needs best, you can also use this beautiful tool:

So, the next time you are subscribing to some observables, see if you can figure out how to achieve what you want without needing to subscribe.