The Reactive Loop

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

Sébastien Cevey

FunctionalJS, London

June 15, 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, Ember, 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/

Observable:
Time-varying value

0

1

2

3

const numbers = [0, 1, 2, 3];

index

0

1

2

3

Arrays

Sequence in space

const fromOne = numbers.map(n => n + 1);

1

2

3

4

const even = plusOne.filter(n => n % 2 == 0);

2

4

// Emits new number every second
const seconds$ = Rx.Observable.interval(1000);

0

1

2

3

seconds

0

1

2

3

const fromOne$ = seconds$.map(n => n + 1);

1

2

3

4

...

...

const even$ = fromOne$.filter(n => n % 2 == 0);

2

4

...

Streams

Sequence in time

const delayed$ = even$.delay(1000);

2

4

...

...

flatMap

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

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")
  );
// ...
const all = Promise.all(catRequests);

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

Promises

Streams

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 [FunctionalJS, London '15]

By Sébastien Cevey

The Reactive Loop [FunctionalJS, London '15]

  • 10,094