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

Made with Slides.com