Observable

Reactive Programming

Тимур Хазамов

Инфраструктура фронтэнда

Maintainer of React UI

ATTENTION

Slides are full of JavaScript

Please be patient and keep calm

!

Typical callbacks

Array callbacks

const floors =
    [9, 10, 8, 7, 6, 5, 4, 3, 2, 1]

function log(...args) {
  console.log(args)
}

floors.forEach(log)

DOM callbacks

const clickerEl = document
    .getElementById('clicker')

function log(...args) {
  console.log(args)
}

clickerEl
    .addEventListener('click', log)

NodeJS callbacks

const fs = require('fs')
const fname = '~/doklad.txt'

function log(err, data) {
  if (err) throw err
  console.log(data)
}

fs.readFile(fname, log)

Promise callbacks

const url =
  'www.vsedokladi.ru/konfur2016'

function log(data) {
  console.log(data)
}

function logErr(err) {
  console.error(err)
}

fetch(url)
    .then(x => x.json())
    .then(log, logErr)

Stream callbacks

const stream = fs.createReadStream('~/doklad.txt')

function log(data) {
  console.log(data)
}

function logErr(err) {
  console.error(err)
}

function logDone() {
  console.log('Hurray!')
}

stream.on('data', log)
stream.on('error', logErr)
stream.on('done', logDone)

and more

3 (three) callbacks

  • onNext(data)
  • onError(error)
  • onComplete()

Super Cool Handler

function superCoolDataSource(
  onNext,
  onError,
  onDone
) {
  /* event handling */
}

Super Cool Handler

function superCoolDataSource(
  onNext,
  onError,
  onDone
) {
  [1, 2, 3].forEach(onNext)
  onDone()
}

Super Cool Handler

function superCoolDataSource(
  onNext,
  onError,
  onDone
) {
  fetch(url)
    .then(onNext, onError)
    .then(onDone)
}

Not cool enough

Observer

const logObserver = {
  next(data) { log(data) },
  error(err) { logErr(err) },
  complete() { logDone() }
}

Observer

const logObserver = {
  next(data) { log(data) },
  error(err) { logErr(err) },
  complete() { logDone() }
}

function supaCoolDataSource(obs) {
  fetch(url)
    .then(obs.next, obs.error)
    .then(obs.done)
}

Observer

const logObserver = {
  next(data) { log(data) },
  error(err) { logErr(err) },
  complete() { logDone() }
}

function subscribe(obs) {
  fetch(url)
    .then(obs.next, obs.error)
    .then(obs.done)
}

Observable

const observable = {
  subscribe(obs) {
    fetch(url)
        .then(obs.next, obs.error)
        .then(obs.done)
  }
}

observable.subscribe(logObserver)

Observable

const observable = {
  subscribe(obs) {
    [9, 10, 8].forEach(obs.next)
    obs.done()
  }
}

observable.subscribe(logObserver)

Observable

const observable = {
  subscribe(obs) {
    fs.readFile(fname, (err, data) => {
      if (err) obs.error(err)
      else obs.next(data)
      obs.done()
    })
  }
}

observable.subscribe(logObserver)

Observable Constructor

function createObservable(subscribe) {
  return {
    subscribe: subscribe
  }
}

const arrayObservable =
  createObservable(obs => {
    [9, 10, 8].forEach(obs.next)
    obs.done()
  })

Be fluent

Observable Extensions

type ObservableExtension =
  (this: Observable, ...args: any[])
    => Observable

function map(mapFn) {
  return createObservable(obs => {
    this.subscribe({
      next(x) { obs.next(mapFn(x)) },
      error(e) { obs.error(e) },
      complete() { obs.complete() }
    })
  })
}

Observable Extensions

function createObservable(subscribe) {
  return {
    subscribe: subscribe,
    map: map
  }
}

const newObservable = arrayObservable
  .map(x => x * 2)

Observable Extensions

function createObservable(subscribe) {
  return {
    subscribe,
    map,
    filter,
    scan,
    delay,
    merge,
    throttle
  }
}

EcmaScript Proposal

Stage 1 Draft August 12 2016

https://tc39.github.io/proposal-observable/

~ 120 Extensions

fromEvent

// Emits clicks happening on the DOM document

var clicks = Rx.Observable
    .fromEvent(document, 'click')

clicks
    .subscribe(x => console.log(x))

merge

filter

debounce

Demo

const bmi$ = Rx.Observable
  .combineLatest(
    weight$, height$,
    (w, h) => w / (h * h)
  )

bmi$
  .subscribe(bmi => {
    const {text, status} =
        getBMIDescription(bmi)
    bmiResult.innerText = text
    bmiResult.className = status
  })
const user$ = request$
  .withLatestFrom(input$, (a, b) => b)
  .filter(Boolean)
  .switchMap((username) =>
    Rx.Observable.fromPromise(
        api.getUser(username)
    ))
  .takeUntil(cancel$)
user$
  .subscribe({
    next(user) {
      cancelButton.style.display = 'none'
      userEl.innerHTML = renderUser(user)
    },
    error(error) {
      userEl.innerHTML = renderError(error)
      cancelButton.style.display = 'none'
      user$.subscribe(this)
    }
  })

Go on... Show the autocomplete

RxJS autocomplete

componentDidMount() {
    this.subscription = this.keyup$
        .debounceTime(500)
        .switchMap(e => 
            http.get(`/autocomplete?q=${e.target.value}`)
        )
        .subscribe(values => this.setState({ values }))
}

Pure JS autocomplete

handleKeyUp(e) {
    e.preventDefault()

    if (this.timeoutId) {
        clearTimeout(this.timeoutId)
    }

    this.timeoutId = setTimeout(() => {
        if (this.xhr) {
            this.xhr.abort()
        }
        const xhr = this.xhr = new XMLHttpRequest()
        xhr.responseType = 'json'
        xhr.onload = e => {
            const values = xhr.response
            this.setState({ values })
            this.xhr = null;
        }
        xhr.open('GET', `/autocomplete?q=${e.target.value}`)
        xhr.send()
    }, 500)

}

Pros

Observable

  • Any number of values
  • Over any amount of time
  • Lazy
  • Cancellable
  • Can be retried/replayed

Some Fun (:


Thanks!
 

Observable

By t1mmaas

Observable

  • 750