The Reactive Loop

https://www.flickr.com/photos/james_clear/5149733984

Sébastien Cevey

JSCamp, Bucharest

June 2, 2015

About me

https://www.flickr.com/photos/theefer/3793936788

Sébastien Cevey

@theefer

 

 

The Grid (AngularJS)

See: Reactive MVC and the Virtual DOM by Andre Medeiros

Grid Inspector (RxJS + virtual-dom)

https://www.flickr.com/photos/76416192@N06/15409726706/

MVC

Separation of concerns

Model – Data

View  Rendering

Controller  Business Logic

Model

View

Ctrl

change events

user events

e.g. Backbone, etc.

Data-binding

Declarative

Bind values between components

Bi-directional

View updates with the Model

Model updates with the View

Uni-directional

View updates with the Model

https://www.flickr.com/photos/horiavarlan/4439643419/

View
Model

View

View
Model

View

e.g. Angular, Ember, Polymer, etc.

Caveat: can only data-bind sync value

Virtual DOM

Complex to keep track of state of DOM and what to change

 

Model

V-DOM

DOM

https://www.flickr.com/photos/torek/3280152297/

= f(Model)

DOM events

Virtual DOM events

DOM updates are expensive

⇒ Update “virtual DOM” (JS objects)
⇒ Apply changes as “patches” to DOM

⇒ Update everything!

e.g. React, etc.

Unidirectional Flow

No conflict, single source of truth

Easier to reason about

e.g. Flux, etc.

Store

View

Action

Dispatcher

change events

invoke callbacks

invoke

invoke

Observer Pattern

https://www.flickr.com/photos/howardignatius/4935713223/

Subject

(…)

observers

(…)

state

state

state

el.addEventListener('input', function(ev) { ... 
<input type="text" ng-change="..." ng-model="...">
return <input type="text" onChange={...}>;

Components depends on state of Subject

Components

state

(…)

Event based

https://www.flickr.com/photos/howardignatius/4935713223/

Hard to compose

State replication, manual management

Inversion of Control

Callbacks

const colours = ['ginger', 'black', 'white'];

function findCat(colour, callback) {
  findImage({query: `${colour} cat`}, (err, image) => {
    callback(err, image.src);
  });
}

var imageSrcs = [];
colours.forEach(colour => {
  findCat(colour, (err, src) => {
    if (err) { alert(err); return; }

    imageSrcs.push(src);

    if (imageSrcs.length === colours.length) {
      xhr.post(url, imageSrcs, (err, resp) => {
        if (err) { alert(err); return; }

        alert('sent!');
      });
    }
  });
});

https://www.flickr.com/photos/matiluba/15523053231/

No return value

Fiddly parallel async

Mutable state (order?)

Nested control flow

Manual error propagation

Completion detection

Promises

const colours = ['ginger', 'black', 'white'];

function findCat(colour) {
  return findImage({query: `${colour} cat`}).
    then(image => image.src);
}

const catRequests = colours.map(findCat);
const all = Promise.all(catRequests);

all.
  then(imageSrcs => {
    return xhr.post(url, imageSrcs);
  }).
  then(() => alert("sent!")).
  catch(error => alert(error));

https://www.flickr.com/photos/elsabordelossegundos/15418211523/

Async value returned

Map async value

Combinators to join multiple Promises

Implicit error propagation

Flatten nested Promises

Promise: Deferred value

Observable streams

https://www.flickr.com/photos/logicalrealist/12555985714/

// Emits new number every second
const seconds$ = Rx.Observable.interval(1000);        // 0, 1, 2, 3, …

const fromOne$    = seconds$.map(n => n + 1);         // 1, 2, 3, 4, …
const twoSeconds$ = seconds$.filter(n => n % 2 == 0); // 0,    2,    …
const delayed$    = seconds$.delay(1000);             //    0, 1, 2, …

Observable:
Time-varying value

const colours$ = Rx.Observable.of('ginger', 'black', 'white');

colours$.
  flatMap(colour => Rx.Observable.fromPromise(findCat(colour))).
  subscribe(
    (x)     => console.log(x),
    (error) => console.log(error),
    ()      => console.log("end")
  );

https://www.flickr.com/photos/logicalrealist/12555985714/

Visualise with RxMarbles

Interactions & State

https://www.flickr.com/photos/logicalrealist/12555985714/

/* Increment total with Ctrl-+, decrement with Ctrl-- */
const keyEvents$ = Rx.Observable.fromEvent(document, 'keydown');
const ctrlKeys$ = keyEvents$.filter(event => event.ctrlKey);
// e.g. +,  +,  +,  -,  +,  -, …
const chars$ = keyEvents$.map(event => event.keyIdentifier);
const ctrlPlus$  = chars$.filter(id => id == 'U+002B'); // '+'
const ctrlMinus$ = chars$.filter(id => id == 'U+002D'); // '-'
const offsets$ = Rx.Observable.merge(
  ctrlPlus$.map( () =>  1),
  ctrlMinus$.map(() => -1)
);
// e.g. 1,  1,  1, -1,  1, -1, …
const total$ = offsets$.scan(0, (sum, n) => sum + n);

total$.subscribe(x => console.log(x));
// e.g. 1,  2,  3,  2,  3,  2, …

Streams & Virtual DOM

// A Simple Clock

import Rx         from 'rx';
import moment     from 'moment';
import h          from 'virtual-dom/h';

Model

View

DOM

?

= f(Model)

https://www.flickr.com/photos/electropod/3392410744/

# Streams
ticker$:            0             1               2

state

const ticker$ = Rx.Observable.interval(50);
const time$ = ticker$.
  map(() => moment()).
  map(t => t.format('HH:mm')).
  distinctUntilChanged();
const tree$ = time$.map(time => h('h1', time));
time$:        "14:36"                       "14:37"
tree$: <h1>14:36</h1>                <h1>14:37</h1>

DOM Patching

// A Simple Clock (ct'd)
// ...
// const tree$ = time$.map(time => h('h1', time));

<div></div>

<h1>14:36</h1>

vdom$
dom$

<div></div>

<h1>14:36</h1>

initialDom
pairs$
patches$

1

2

<h1>14:37</h1>

3

1

2

2

3

1 => 2

2 => 3

<h1>14:37</h1>

import diff       from 'virtual-dom/diff';
import patch      from 'virtual-dom/patch';
import virtualize from 'vdom-virtualize';
const out = document.getElementById('out');
const initialDom = virtualize(out);
const vdom$    = tree$.startWith(initialDom);
const pairs$   = vdom.bufferWithCount(2, 1);
const patches$ = pairs$.map(([last, current]) => diff(last, current));
const dom$     = patches$.reduce((node, patches) => patch(node, patches), out);
dom$.subscribeOnError(err => console.error(err));

User intents

https://www.flickr.com/photos/dskley/7717799328/

Model

View

DOM

Intents

events

actions

state

function view(model) {
  const clicks$ = new Rx.Subject;
  const tree$ = model.time$.map(time => h('h1', {
    onclick: ev => clicks$.onNext(ev)
  }, time));

  return {
    tree$,
    events: {clicks$}
  };
}
function intents(view) {
  return {
    toggle$: view.events.clicks$.map(() => true)
  };
}
function model(intents) {
  // combine intents.toggle$ with initial state
  const time$ = ... ;
  return {time$};
}

DOM events

Live        coding

https://www.flickr.com/photos/73416633@N00/2411322052/

Demo as a graph

(query$)

(free$)

input

query$

free$

checkbox

labelled checkbox

filters

results

heading

(query$)

image results

(results$)

view

results$

https://www.flickr.com/photos/aukirk/9233111519/

Reactive Loop

Fully declarative

Explicit dependencies

Clear state ownership, no duplication

Only re-renders what changed

 

Natively async

Easy throttle, flatMap, etc.

Composition of streams

Future

https://www.flickr.com/photos/iceninejon/3836986684/

Read/update state in URL

Unit test setup

Library of reusable components

Web Workers

requestAnimationFrame

Web Components integration

https://www.flickr.com/photos/rollercoasterphilosophy/3419051308/

No silver bullet

 

Libraries vs Frameworks

 

Reactive vs Observer Pattern

 

Start small (Promises, rx.angular, etc)

References

https://www.flickr.com/photos/flyingblogspot/15361704293/

Thank you!

Sébastien Cevey

@theefer

https://www.flickr.com/photos/josemanuelerre/6088074577

The Reactive Loop [JSCamp, Bucharest '15]

By Sébastien Cevey

The Reactive Loop [JSCamp, Bucharest '15]

  • 4,525