https://www.flickr.com/photos/james_clear/5149733984
Sébastien Cevey
JSCamp, Bucharest
June 2, 2015
https://www.flickr.com/photos/theefer/3793936788
See: Reactive MVC and the Virtual DOM by Andre Medeiros
https://www.flickr.com/photos/76416192@N06/15409726706/
Model – Data
View – Rendering
Controller – Business Logic
Model
View
Ctrl
⇣
⇢
change events
user events
e.g. Backbone, etc.
Bind values between components
View updates with the Model
Model updates with the View
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
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.
No conflict, single source of truth
Easier to reason about
e.g. Flux, etc.
Store
View
Action
Dispatcher
⇣
change events
invoke callbacks
⇠
invoke
invoke
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
(…)
⇢
⇢
⇢
https://www.flickr.com/photos/howardignatius/4935713223/
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
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
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, …
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
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, …
// 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>
// 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));
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
https://www.flickr.com/photos/73416633@N00/2411322052/
(query$)
(free$)
input
query$
free$
checkbox
labelled checkbox
filters
results
heading
(query$)
image results
(results$)
view
results$
https://www.flickr.com/photos/aukirk/9233111519/
Explicit dependencies
Clear state ownership, no duplication
Only re-renders what changed
Easy throttle, flatMap, etc.
Composition of streams
https://www.flickr.com/photos/iceninejon/3836986684/
https://www.flickr.com/photos/rollercoasterphilosophy/3419051308/
https://www.flickr.com/photos/flyingblogspot/15361704293/
Sébastien Cevey
https://www.flickr.com/photos/josemanuelerre/6088074577