Boost your

RxJS

experience with

Custom Operators

08.09.2019, GrillJS

Chris Trześniewski

Senior Fronend Developer @Scalac

http://bit.ly/rx-grilljs

What's an Observable

 

  • Stream
  • Promise on steroids
  • Function that tie observer to a producer

Data delivered over time

 

What are operators?

Operators are the things you put in front of your stream before getting values out of it

~Ryan Chenkie

Operators

  • filter
  • debounce
  • throttle

Filtering

Operators

  • map
  • scan (reduce)
  • switchMap
  • mergeMap

Transforming

Operators

  • combineLatest
  • withLatestFrom
  • zip

Combining

Operators

  • timer
  • interval
  • from / of
  • create

Creating

Operators

Operators

const source$: Observable<number> = timer(1000);

const result$: Observable<number> = source$
  .pipe(
    filter(v => v % 3 === 1),
    map(v => v * 7)
  );

There's over 100 operators!

Do we even need custom ones?

Custom operators

  • Creating specific operators based on existing ones
  • Extract multiple operators
    • more meaningful
    • reusable
  • Create missing pieces ourselves

We can benefit by

Operators

Under the hood

  • Function
  • Taking observable as an argument
  • Returns observable
type OperatorFunction<T, R> =
 (source: Observable<T>)): Observable<R>
const identity =
 (source: Observable<any>) => source

Operators

Under the hood

Custom operators

How do we put that in real life?

Custom operators

  • static operators ie. notNull, isTruthy operator

Extract most common use cases

const notNull = filter(v => v !== null)
const isTruthy = filter(v => !!v)

Custom operators

  • parametrised operators ie. `multiply`, `power`

Extract most common use cases

const multiply =
  m => map(v => v * m)

const power =
  p => map(v => Math.pow(v, p))

Custom operators

Group multiple operators into one

const myOperator =
  <T,R>(source: Observable<T>) =>
    source.pipe(operator1, operator2)

Custom operators

Group multiple operators into one

const oddValueTimesSeven =
  (num$: Observable<number>) =>
    num$.pipe(

      filter(v => v % 2 === 1),

      map(v => v * 7),
    
)

Custom operators

Group multiple operators into one

const extractData =
  <T>(response$: Observable<{data: T, loading: boolean}>)
    => response$.pipe(
        isTruthy,
        filter(response => !response.loading),
        map(v => v.data),
      )

Custom operators

Create from scratch

const myOperator =
  (source: Observable) => 
    new Observable((observer: Subscriber) => {
     source.subscribe({

        next(x) {/* … */ observer.next(…)},

        error(err) {/* … */ observer.error(…)},

        complete(x) {/* … */ observer.complete(…)},

      })
    })

Custom operators

Create from scratch

const customMap = <T, R>(
  project: (value: T) => R
) => (source: Observable<T>) =>
  new Observable<R>(observer =>
    source.subscribe({
      next(x) { observer.next(project(x)) },
      error(err) { observer.error(err) },
      complete() { observer.complete() }
    })
  )

Custom operators

Create from scratch

const customMap = <T, R>(
  project: (value: T) => R
): OperatorFunction<T, R> =>
  (source: Observable<T>): Observable<R> => {
    return source.lift(
      new CustomMapOperator<T, R>(project)
    );
  };

class CustomMapOperator<T, R> implements Operator<T, R> {
  constructor(private project: (value: T) => R) {}

  call(sub: Subscriber<R>, src: any): TeardownLogic {
    return src.subscribe(
      new CustomMapSubscriber(sub, this.project)
    );
  }
}
/* ... */
  (source: Observable<T>): Observable<R> => {
    return source.lift(
      new CustomMapOperator<T, R>(project)
    );
  };

class CustomMapOperator<T, R> implements Operator<T, R> {
  constructor(private project: (value: T) => R) {}

  call(subscriber: Subscriber<R>, source: any): TeardownLogic {
    return source.subscribe(
      new CustomMapSubscriber(subscriber, this.project)
    );
  }
}

class CustomMapSubscriber<T, R> extends Subscriber<T> {
  constructor(
    destination: Subscriber<R>,
    private project: (value: T) => R
  ) {
    super(destination);
  }

  protected _next(value: T): void {
    this.destination.next(this.project(value));
  }
}

Custom operators

Create from scratch

  • bufferDelay
     
  • given a delay time parameter
  • if more values are emitted in given time slot
    they are spaced evenly with `delayTime`
  • if value is emitted after specified delay time
    it's emitted right away

Summary

  • Common use case ==> more descriptive name
  • Chain of operations ==> one descriptive operator
  • Our brand new custom operators
  • Now we know how to harness the power of operators

but remember...

  • Don't overuse it
     
  • Test it!

Thank you!

 

Any question?

 

GrillJS 2019 - Boost your RxJS experience with custom operators

By Chris Trześniewski

GrillJS 2019 - Boost your RxJS experience with custom operators

  • 27
Loading comments...

More from Chris Trześniewski