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
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
https://www.flickr.com/photos/josemanuelerre/6088074577
The Reactive Loop [JSCamp, Bucharest '15]
By Sébastien Cevey
The Reactive Loop [JSCamp, Bucharest '15]
- 4,731