Switch from pull to push-based approach
with RxJS
Wojciech Trawiński
Poznań, 2019
About me
JavaScript developer working at 7N for Roche company.
Passionate of Angular, RxJS, TypeScript and functional programming.
About me
Owner of JavaScript everyday blog
Writer for Angular In Depth and Angular Love blogs
JavaScript developer working at 7N for Roche company.
Passionate of Angular, RxJS, TypeScript and functional programming.
Reactive programming
... a declarative programming paradigm
concerned with data streams and the propagation of change.
Reactive programming
... a declarative programming paradigm
concerned with data streams and the propagation of change.
Object Oriented Programming
Reactive Programming
everything is a class
everything is a stream
RxJS
In a nutshell:
- Reactive Extensions Library for JavaScript,
- make it easier to compose asynchronous or callback-based code,
- Observable is the most basic building block.
RxJS
In a nutshell:
- Reactive Extensions Library for JavaScript,
- make it easier to compose asynchronous or callback-based code,
- Observable is the most basic building block.
Observable
Observer
subscribe
notifications
Observer
Types of observable notifications:
- next,
- error,
- complete.
Observer
Types of observable notifications:
- next,
- error,
- complete.
An observer may be interested in different kinds of notifications
and corresponding callbacks may be provided in the following ways:
Observer
Types of observable notifications:
- next,
- error,
- complete.
An observer may be interested in different kinds of notifications
and corresponding callbacks may be provided in the following ways:
as object
interval(1000).subscribe({
next: console.log,
error: console.error,
complete: () => console.log('completed')
});
Observer
Types of observable notifications:
- next,
- error,
- complete.
An observer may be interested in different kinds of notifications
and corresponding callbacks may be provided in the following ways:
as object
interval(1000).subscribe({
next: console.log,
error: console.error,
complete: () => console.log('completed')
});
as successive arguments
interval(1000).subscribe(
console.log,
console.error,
() => console.log('completed')
);
Observable
A representation of any set of values
over any amount of time
Observable
A representation of any set of values
over any amount of time
RxJS provides a large number of creational operators:
- of, from, pairs (streaming existing data),
- interval, timer, range (generating data),
- from, fromEvent, ajax (hooking into existing api),
- merge, combineLatest, zip (combining existing streams).
Subjects
… [multicasting] is the primary use case for Subjects in RxJS
Subjects
… [multicasting] is the primary use case for Subjects in RxJS
import { Subject, interval } from 'rxjs';
const source$ = interval(1000);
const mySubject = new Subject();
source$.subscribe(mySubject);
mySubject.subscribe(val => console.log(`#1 ${val}`));
setTimeout(() => {
mySubject.subscribe(val => console.log(`#2 ${val}`));
}, 5000);
Operators
A function which accepts a stream and returns a new observable
Operators
A function which accepts a stream and returns a new observable
import { interval } from 'rxjs';
import { throttleTime } from 'rxjs/operators';
const fastSource$ = interval(10);
const result$ = fastSource$.pipe(
throttleTime(2000)
);
result$.subscribe(console.log);
Let's code!
Task description
- you are given an endpoint (jsonplaceholder.typicode.com/users/:id) which returns a user's info for a given id,
- typing a query into an input element results in a new search,
- 500ms have to elapse after a user stopped typing before making an http request,
- an http request shouldn’t be made if a query string hasn’t changed,
- only the most recent query string is considered.
export class UserService {
private _apiUrl = 'your-api-url';
private _isLoading = true;
private _user: User | null = null;
get isLoading(): boolean {
return this._isLoading;
}
get hasUser(): boolean {
return this._user !== null;
}
get user(): User | null {
return this._user ? { ...this._user } : null;
}
}
Service
Pull-based approach
export interface User {
id: number;
name: string;
username: string;
email: string;
}
Model
Pull-based approach
export class UserService {
private _apiUrl = 'your-api-url';
private _isLoading = true;
private _user: User | null = null;
get isLoading(): boolean {
return this._isLoading;
}
get hasUser(): boolean {
return this._user !== null;
}
get user(): User | null {
return this._user ? { ...this._user } : null;
}
}
Service
export class UserService {
private _apiUrl = 'your-api-url';
private _isLoading = true;
private _user: User | null = null;
get isLoading(): boolean {
return this._isLoading;
}
get hasUser(): boolean {
return this._user !== null;
}
get user(): User | null {
return this._user ? { ...this._user } : null;
}
loadUser(id: string) {
this._isLoading = true;
fetch(`${this.apiUrl}/users/${id}`)
.then(user => user.json())
.then(user => {
this._user = user;
this._isLoading = false;
});
}
}
Service
Pull-based approach
export interface User {
id: number;
name: string;
username: string;
email: string;
}
Model
Pull-based approach
const userService = new UserService();
function render() {
appDiv.innerHTML = `
-loading: ${userService.isLoading} <br>
-has user: ${userService.hasUser} <br>
-user: ${JSON.stringify(userService.user)}
`
}
Usage
export interface User {
id: number;
name: string;
username: string;
email: string;
}
Model
export class UserService {
private _apiUrl = 'your-api-url';
private _isLoading = true;
private _user: User | null = null;
get isLoading(): boolean {
return this._isLoading;
}
get hasUser(): boolean {
return this._user !== null;
}
get user(): User | null {
return this._user ? { ...this._user } : null;
}
loadUser(id: string) {
this._isLoading = true;
fetch(`${this.apiUrl}/users/${id}`)
.then(user => user.json())
.then(user => {
this._user = user;
this._isLoading = false;
});
}
}
Service
Advantages:
- easy to reason about,
- simple to implement.
Pull-based approach
Advantages:
- easy to reason about,
- simple to implement.
Disadvantages:
- client has to schedule when to pull data from the service,
- unnecessary updates,
- rendering stale data,
- computation effort within getters w/o memoization.
Pull-based approach
Push-based approach
Service
export class UserService {
private apiUrl = 'your-api-url';
private loadingSubject = new BehaviorSubject(true);
private userIdSubject = new Subject<string>();
readonly user$ = ...
readonly isLoading$ = this.loadingSubject.pipe(
distinctUntilChanged()
);
readonly hasUser$ = this.user$.pipe(
map(Boolean)
);
loadUser(id: string) {
this.userIdSubject.next(id);
}
}
Push-based approach
Service
export interface User {
id: number;
name: string;
username: string;
email: string;
}
Model
export class UserService {
private apiUrl = 'your-api-url';
private loadingSubject = new BehaviorSubject(true);
private userIdSubject = new Subject<string>();
readonly user$ = ...
readonly isLoading$ = this.loadingSubject.pipe(
distinctUntilChanged()
);
readonly hasUser$ = this.user$.pipe(
map(Boolean)
);
loadUser(id: string) {
this.userIdSubject.next(id);
}
}
Push-based approach
Service
export interface User {
id: number;
name: string;
username: string;
email: string;
}
Model
readonly user$ = this.userIdSubject.pipe(
tap(() => this.setLoadingFlag(true)),
switchMap(id => this.getUserStream(id)),
tap(() => this.setLoadingFlag(false)),
startWith(null),
shareReplay(1)
);
User stream
export class UserService {
private apiUrl = 'your-api-url';
private loadingSubject = new BehaviorSubject(true);
private userIdSubject = new Subject<string>();
readonly user$ = ...
readonly isLoading$ = this.loadingSubject.pipe(
distinctUntilChanged()
);
readonly hasUser$ = this.user$.pipe(
map(Boolean)
);
loadUser(id: string) {
this.userIdSubject.next(id);
}
}
Push-based approach
Service
export interface User {
id: number;
name: string;
username: string;
email: string;
}
Model
readonly user$ = this.userIdSubject.pipe(
tap(() => this.setLoadingFlag(true)),
switchMap(id => this.getUserStream(id)),
tap(() => this.setLoadingFlag(false)),
startWith(null),
shareReplay(1)
);
User stream
private setLoadingFlag(isLoading: boolean) {
this.loadingSubject.next(isLoading);
}
export class UserService {
private apiUrl = 'your-api-url';
private loadingSubject = new BehaviorSubject(true);
private userIdSubject = new Subject<string>();
readonly user$ = ...
readonly isLoading$ = this.loadingSubject.pipe(
distinctUntilChanged()
);
readonly hasUser$ = this.user$.pipe(
map(Boolean)
);
loadUser(id: string) {
this.userIdSubject.next(id);
}
}
Push-based approach
export class UserService {
private apiUrl = 'your-api-url';
private loadingSubject = new BehaviorSubject(true);
private userIdSubject = new Subject<string>();
readonly user$ = ...
readonly isLoading$ = this.loadingSubject.pipe(
distinctUntilChanged()
);
readonly hasUser$ = this.user$.pipe(
map(Boolean)
);
loadUser(id: string) {
this.userIdSubject.next(id);
}
}
Service
export interface User {
id: number;
name: string;
username: string;
email: string;
}
Model
readonly user$ = this.userIdSubject.pipe(
tap(() => this.setLoadingFlag(true)),
switchMap(id => this.getUserStream(id)),
tap(() => this.setLoadingFlag(false)),
startWith(null),
shareReplay(1)
);
User stream
private setLoadingFlag(isLoading: boolean) {
this.loadingSubject.next(isLoading);
}
private getUserStream(id: string): Observable<User> {
return ajax(`${this.apiUrl}/users/${id}`).pipe(
map(({ response }) => response)
);
}
Push-based approach
Implementing remaining task requirements
Push-based approach
readonly user$ = this.userIdSubject.pipe(
debounceTime(500),
tap(() => this.setLoadingFlag(true)),
switchMap(id => this.getUserStream(id)),
tap(() => this.setLoadingFlag(false)),
startWith(null),
shareReplay(1)
);
Implementing remaining task requirements
500 ms have to elapse
after a user stopped typing
before making an http request
Push-based approach
readonly user$ = this.userIdSubject.pipe(
debounceTime(500),
tap(() => this.setLoadingFlag(true)),
switchMap(id => this.getUserStream(id)),
tap(() => this.setLoadingFlag(false)),
startWith(null),
shareReplay(1)
);
Implementing remaining task requirements
readonly user$ = this.userIdSubject.pipe(
debounceTime(500),
distinctUntilChanged(),
tap(() => this.setLoadingFlag(true)),
switchMap(id => this.getUserStream(id)),
tap(() => this.setLoadingFlag(false)),
startWith(null),
shareReplay(1)
);
500 ms have to elapse
after a user stopped typing
before making an http request
Http request shouldn’t be made if a query string hasn’t changed
Advantages:
- inversion of control,
- updates as a result of new notifications,
- each client notified immediately,
- computation performed only for new values,
- large number of operators encapsulating common logic.
Push-based approach
Advantages:
- inversion of control,
- updates as a result of new notifications,
- each client notified immediately,
- computation performed only for new values,
- large number of operators encapsulating common logic.
Disadvantages:
- takes time to get familiar with RxJS,
- thinking in a reactive way may be tough.
Push-based approach
Thanks
for your attention!
wojtrawi@gmail.com
Switch from pull to push-based approach with RxJS
By wojtrawi
Switch from pull to push-based approach with RxJS
- 365