Pattern Intoduction to Rxjs
Recently, we upgraded our mobile app from Ionic 1 to Ionic 5.
Wrapping our head around Angular, after working for so long in angular js, wasn't too big of a hurdle.
The problem with Rxjs - it's difficult to find examples of how to do stuff.
Take for example, infinite scroll.
What is infinite scroll?
When the user opens the page showing the rows of data - you will load up the first 10 rows.
When the user has scrolled near the end of the page, you can check if a new page is there and if yes, make an api call to fetch the next page and append it to the end of the pre existing list.
And not just vanilla infinite scroll, it was chocolate cake infinite scroll with cherry on top-
Infinite scroll to be present as default
Filters and sorting can be applied from the UI
When search is used, all of the data is to be pulled in (so that we can filter everything by text)
On pull to refresh, invalidate the cache and reload data
We had our work cut out for us and doing it the right way was very necessary since this is used in very critical pages.
With Rxjs - things started to feel easier once we got around learning how to think about them.
This blog won't contain any tutorials for Rxjs but make you familiar with two design patterns which will help you understand what is going on when you start working with rxjs.
What are design patterns?
Design patterns are meta structures that you use when approaching a very specific kind of problem.
The examples I have presented here are in typescript, but you can transfer them to pretty much any other language.
The two patterns you must know -
Observer
Iterator
The Observer Pattern
An Observer pattern is where you have an Observable where you observe something and call a method called notify as an event occurs.
All the update methods in the Observers associated with the Observable are called whenever the notify is triggered by the Observable.
The pattern classes-
interface Observer {
update(e: any): void;
}
abstract class Subject {
observers: Observer[] = [];
attach(ob: Observer) {
this.observers.push(ob);
}
notify(e) {
this.observers.forEach(observer => {
observer.update(e);
});
}
}
Concrete implementations -
class LoggerObserver implements Observer {
update(e) {
console.log('logger', e);
}
}
class DoubleLoggerObserver implements Observer {
update(e) {
console.log('double logger', e);
console.log('double logger', e);
}
}
Usage -
const inputElement: HTMLElement = document.getElementById("sample");
class FormInputSubject extends Subject {
constructor(el: HTMLElement) {
super();
el.addEventListener("keydown", event => {
this.notify(event);
});
}
}
const formObservable = new FormInputObservable(inputElement);
formObservable.attach(new DoubleLoggerObserver());
formObservable.attach(new LoggerObserver());
If you look at the usage - you will see that FormInputObservable does not contain any code regarding what happens when the event occurs
This is exactly what the Observer pattern does -
it helps decouple an event from what is to happen when the event occurs.
Working Sample - https://stackblitz.com/edit/typescript-tzqqqk?file=index.ts
The Iterator Design Pattern
The iterator pattern is used to traverse a collection, where the underlying data structure may be anything ( an array, tree etc ).
Say you have a tree data structure.
You can traverse this in many ways - depth first, breadth first etc
Having an uniform way to traverse this is what the iterator pattern provides you.
interface AIterator<T> {
// Return the current element.
current(): T;
// Return the current element and move forward to the next element.
next(): T;
// Return the key of the current element.
key(): number;
}
The concrete implementation -
class NumberArrayIterator implements AIterator<number> {
arr: number[];
currentIndex: number;
constructor(arr: number[]) {
this.arr = arr;
this.currentIndex = -1;
}
next(): number {
if (this.arr[this.currentIndex + 1]) {
this.currentIndex = this.currentIndex + 1;
return this.arr[this.currentIndex];
}
return null;
}
current(): number {
return this.arr[this.currentIndex];
}
key(): number {
return this.currentIndex;
}
}
const a = new NumberArrayIterator([1, 2, 3, 4, 5]);
while (a.next()) {
console.log(a.current());
}
The iterator pattern is awesome in the sense that you can think of anything you want to iterate on and model it in this format.
Working Sample - https://stackblitz.com/edit/typescript-6cbbbc?file=index.ts
Now, back to Rxjs.
Rxjs is based around something called an Observable.
An Observable in Rxjs is basically a combination of the Observer and Iterator.
To create an Observable
const ob$ = new Observable(subscriber => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
subscriber.next(4);
// it doesn't matter if this set interval goes on forever. An observer stops emitting once it has completed
setInterval(()=> {
subscriber.next(2);
subscriber.next(3);
subscriber.next(4);
}, 100);
setTimeout(()=> {
subscriber.complete();
});
});
const sub1 = ob$.pipe(
filter(x => x/2)
).subscribe(val => {
console.log(‘first’, val);
});
const sub2 = ob$.subscribe(val => {
console.log(‘second’, val);
console.log(‘second’, val);
});
Like the Observer design pattern - an Observable decouples the creation of the events from how you would handle them.
And it allows you to process the events that would come from this observable in the form of a stream - similar to the iterator pattern.
The subscribe is nothing but a for Each on events that arrive over time till the iterator reaches its end or the subscription is stopped manually.
This gives rise to a very powerful pattern that you can use the stream of events that arrive over time and mould them using operators - similar to how you use map, filter and reduce on arrays.
For a in depth study, I would highly recommend
- https://springframework.guru/gang-of-four-design-patterns/
- https://refactoring.guru/design-patterns/