Imperative vs Declarative Programming with RxJS: A Search Filter
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 codeImperative 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:
- We set up a form control for the search field
- We get the data for our clients list by
subscribing
to theObservable
from theClientService
- We
subscribe
to thevalueChanges
observable for the search field - Whenever we receive a new value, we update our
filteredClients
data that the list uses - 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:
- We set up a form control for the search field
- We obtain an observable stream of clients
- We obtain an observable stream of the search term
- 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
.