Sébastien Cevey

@theefer

hard problems of building UIs

Async

``````function updateConverted() {
const amount = \$('.amount').val();
const result = amount * rate;
\$('.converted').text(result);
}

\$('.amount').keyup(updateConverted);

updateConverted(); // init``````
``````\$('.reset').click(function() {
\$('.amount').val(1);
\$('.currency').val('EUR');
updateRate();
});``````
``````let rate;

\$('.currency').change(function() {
const currency = this.value;
updateRate(currency);
});

function updateRate(currency) {
lookupRate(currency).then(newRate => {
\$('.rate').text(newRate);
rate = newRate;
updateConverted();
});
}

updateRate('EUR'); // init``````
`(loading)`

Reactive Programming

``````a := 1
b := 2
c := a + b  // c == 3

a := 3      // c == 5``````

Values that vary over time

aka Observable Streams

``````a := 1
b := 2
c := a + b  // c == 3

a := 3      // c == 5``````
``````// Emits a new incrementing number every second
const seconds\$ = Rx.Observable.interval(1000);``````

...

seconds

0

0

1

2

3

4

1

2

3

4

``````// Emits a new incrementing number every second
const seconds\$ = Rx.Observable.interval(1000);``````
``const fromTen\$ = seconds\$.map(n => n + 10);``

seconds

0

0

1

2

3

4

1

2

3

4

...

10

11

12

13

14

``````// Emits a new incrementing number every second
const seconds\$ = Rx.Observable.interval(1000);``````
``const fromTen\$ = seconds\$.map(n => n + 10);``
``const even\$ = fromTen\$.filter(n => n % 2 == 0);``

seconds

0

0

1

2

3

4

1

2

3

4

10

11

12

13

14

...

10

12

14

``````// Emits a new incrementing number every second
const seconds\$ = Rx.Observable.interval(1000);``````
``const fromTen\$ = seconds\$.map(n => n + 10);``
``const even\$ = fromTen\$.filter(n => n % 2 == 0);``
``const delayed\$ = even\$.delay(1000);``

seconds

0

0

1

2

3

4

1

2

3

4

10

11

12

13

14

10

12

14

10

12

14

RxMarbles.com

``````const seconds\$ = Rx.Observable.interval(1000);

seconds\$.take(3).subscribe(sec => {
console.log("second: ", sec);
});

/* Outputs
*
*   second: 0
*   second: 1
*   second: 2
*
* and terminates.
*/``````
``````
// Intent:
//   Observable< Function< State => State > >
const amount\$ = intents.updateAmount\$.

``````
``function model(intents) {``

Model

Intents

State as streams

``````  const init = (initialState) => initialState;
// Intent:
//   Observable< Function< State => State > >
const amount\$ = intents.updateAmount\$.
startWith(init).
scan((state, func) => func(state), 1);``````
``````
// Intent:
//   Observable< Function< State => State > >
const amount\$ = intents.updateAmount\$.

scan((state, func) => func(state), 1);``````
``````  const init = (initialState) => initialState;
// Intent:
//   Observable< Function< State => State > >
const amount\$ = intents.updateAmount\$.
startWith(init).
scan((state, func) => func(state), 1);``````
``````  const currency\$ = intents.updateCurrency\$.
startWith(init).
scan((state, func) => func(state), 'EUR');``````
``function model(intents) {``

Model

Intents

State as streams

``````  const init = (initialState) => initialState;
// Intent:
//   Observable< Function< State => State > >
const amount\$ = intents.updateAmount\$.
startWith(init).
scan((state, func) => func(state), 1);``````
``````  const currency\$ = intents.updateCurrency\$.
startWith(init).
scan((state, func) => func(state), 'EUR');``````
``````  const rate\$ = currency\$.
map(currency => lookupRate(currency)).
flatMap(p => Rx.Observable.fromPromise(p));``````
``function model(intents) {``

Model

Intents

State as streams

``````  const init = (initialState) => initialState;
// Intent:
//   Observable< Function< State => State > >
const amount\$ = intents.updateAmount\$.
startWith(init).
scan((state, func) => func(state), 1);``````
``````  const currency\$ = intents.updateCurrency\$.
startWith(init).
scan((state, func) => func(state), 'EUR');``````
``````  const rate\$ = currency\$.
map(currency => lookupRate(currency)).
flatMap(p => Rx.Observable.fromPromise(p));``````
``function model(intents) {``
``````  const converted\$ = Rx.Observable.combineLatest(
amount\$, rate\$,
(amount, rate) => amount * rate);
``````

Model

Intents

State as streams

``````  const init = (initialState) => initialState;
// Intent:
//   Observable< Function< State => State > >
const amount\$ = intents.updateAmount\$.
startWith(init).
scan((state, func) => func(state), 1);``````
``````  const currency\$ = intents.updateCurrency\$.
startWith(init).
scan((state, func) => func(state), 'EUR');``````
``````  const rate\$ = currency\$.
map(currency => lookupRate(currency)).
flatMap(p => Rx.Observable.fromPromise(p));``````
``function model(intents) {``
``````  const converted\$ = Rx.Observable.combineLatest(
amount\$, rate\$,
(amount, rate) => amount * rate);
``````

Model

Intents

State as streams

``````  const rateLoading\$ = Rx.Observable.merge(
currency\$.map(true),
rate\$.map(false)
);
``````
``````  const init = (initialState) => initialState;
// Intent:
//   Observable< Function< State => State > >
const amount\$ = intents.updateAmount\$.
startWith(init).
scan((state, func) => func(state), 1);``````
``````  const currency\$ = intents.updateCurrency\$.
startWith(init).
scan((state, func) => func(state), 'EUR');``````
``````  const rate\$ = currency\$.
map(currency => lookupRate(currency)).
flatMap(p => Rx.Observable.fromPromise(p));``````
``function model(intents) {``
``````  return {
amount\$, currency\$, rate\$, converted\$,
};
}``````
``````  const converted\$ = Rx.Observable.combineLatest(
amount\$, rate\$,
(amount, rate) => amount * rate);
``````

Model

Intents

State as streams

``````  return {
amount\$: amount\$,
currency\$: currency\$,
...
};``````
``````  const rateLoading\$ = Rx.Observable.merge(
currency\$.map(true),
rate\$.map(false)
);
``````
``````const tree = h('main', [
h('form', [
h('label', [
h('span.label', 'Amount'),
h('input', {
type: 'text',
value: '5',
oninput: function(ev) { ... }
})),
' GBP'
]),
``````

Matt-Esch/virtual-dom

``````<main>
<form>
<label>
<span class="label">Amount</span>
<input
type="text"
value="5"
oninput="..."
/>
GBP
</label>``````
``function view() {``
``````  const events = {
amountChanged\$:   new Rx.Subject(),

};
``````

Model

Intents

View

tree\$

``````  function tree\$(model) {
return h('main', [
h('form', [
h('label', [
h('span.label', 'Amount'),
h('input', {
type: 'text',
value: ? amount ?,

}),
' GBP']),

}
``````

Interactions & DOM as streams

``````  function tree\$(model) {
return h\$('main', [
h\$('form', [
h\$('label', [
h('span.label', 'Amount'),
model.amount\$.map(amount => h('input', {
type: 'text',
value: amount,
oninput: (event) => {
???
}})),
' GBP']),

}
``````
``````  function tree\$(model) {
return h('main', [
h('form', [
h('label', [
h('span.label', 'Amount'),
model.amount\$.map(amount => h('input', {
type: 'text',
value: amount,

})),
' GBP']),

}
``````
``````  function tree\$(model) {
return h\$('main', [
h\$('form', [
h\$('label', [
h('span.label', 'Amount'),
model.amount\$.map(amount => h('input', {
type: 'text',
value: amount,

})),
' GBP']),

}
``````
``````  function tree\$(model) {
return h\$('main', [
h\$('form', [
h\$('label', [
h('span.label', 'Amount'),
model.amount\$.map(amount => h('input', {
type: 'text',
value: amount,
oninput: (event) => {
events.amountChanged\$.onNext(event)
}})),
' GBP']),

}
``````

events

``function view() {``
``````  return {
tree\$, events
};
}``````
``````  const events = {
amountChanged\$:   new Rx.Subject(),
currencyChanged\$: new Rx.Subject(),
resetClicked\$:    new Rx.Subject()
};
``````

Model

Intents

View

tree\$

``````  function tree\$(model) {
return h\$('main', [
h\$('form', [
h\$('label', [
h('span.label', 'Amount'),
model.amount\$.map(amount => h('input', {
type: 'text',
value: amount,
oninput: (event) => {
events.amountChanged\$.onNext(event)
}})),
' GBP']),
// ...
h\$('div.converted', [
h('span.label', 'Converted'),
model.converted\$,
' ',
model.currency\$]),

}
``````

Interactions & DOM as streams

``````  function tree\$(model) {
return h\$('main', [
h\$('form', [
h\$('label', [
h('span.label', 'Amount'),
model.amount\$.map(amount => h('input', {
type: 'text',
value: amount,
oninput: (event) => {
events.amountChanged\$.onNext(event)
}})),
' GBP']),

}
``````
``````  function tree\$(model) {
return h\$('main', [
h\$('form', [
h\$('label', [
h('span.label', 'Amount'),
model.amount\$.map(amount => h('input', {
type: 'text',
value: amount,
oninput: (event) => {
events.amountChanged\$.onNext(event)
}})),
' GBP']),
// ...
h\$('div.converted', [
h('span.label', 'Converted'),
model.converted\$,
' ',
model.currency\$]),
h('button.reset', {
type: 'button',
onclick: sink\$(events.resetClicked\$)
}, 'Reset')]);
}
``````

events

``function intents(events) {``
``````  const updateAmount\$ = Rx.Observable.merge(
events.amountChanged\$.
map(event => event.target.value).
map(newAmount => current => newAmount),
events.resetClicked\$.
map(event => current => 1),
events.incrementClicked\$.
map(event => current => current + 1),
events.decrementClicked\$.
map(event => current => current - 1)
);``````

Intents

events

tree\$

View

Model

Intents as streams

``````  const updateAmount\$ = Rx.Observable.merge(
events.amountChanged\$.
map(event => event.target.value).
map(newAmount => current => newAmount),
events.resetClicked\$.
map(event => current => 1)

);``````
``````  const updateAmount\$ = Rx.Observable.merge(
events.amountChanged\$.
map(event => event.target.value).
map(newAmount => current => newAmount)

);``````

old state

new state

=>

``function intents(events) {``
``````  return {
updateAmount\$,
updateCurrency\$
};
}``````
``````  const updateAmount\$ = Rx.Observable.merge(
events.amountChanged\$.
map(event => event.target.value).
map(newAmount => current => newAmount),
events.resetClicked\$.
map(event => current => 1),
events.incrementClicked\$.
map(event => current => current + 1),
events.decrementClicked\$.
map(event => current => current - 1)
);``````

Intents

``````  const updateCurrency\$ = Rx.Observable.merge(
events.currencyChanged\$.
map(event => event.target.value).
map(newCurrency => current => newCurrency),
events.resetClicked\$.
map(event => current => 'EUR')
);
``````

tree\$

View

Model

Intents as streams

events

``````const theView    = view();
const theIntents = intents(theView.events);
const theModel   = model(theIntents);
const tree\$      = theView.tree\$(theModel);

const out = document.getElementById('out');

// Helper to subscribe & apply patches to the DOM
renderToDom(tree\$, out);

// Observable< VDOM > => Observable< VPatches >``````

Intents

DOM

tree\$

View

Model

events

Performance

``````  const amount\$ = intents.updateAmount\$.
startWith(init).
scan((state, func) => func(state), 1).
distinctUntilChanged().
debounce(300 /* ms */);``````
``````  // [...]
h\$('div.converted', [
h('span.label', 'Converted'),
model.converted\$,
' ',
model.currency\$]),
// [...]
``````

viewport

tableHeight\$

itemCount\$

items\$

searchQuery\$

resizeEvents\$

windowWidth\$

columnCount\$

viewportTop\$

scrollEvents\$

resizeEvents\$

viewportHeight\$

cellPosition\$(item)

itemIndex\$

items\$

searchQuery\$

resizeEvents\$

windowWidth\$

columnCount\$

`top, left, width, height`

Questions?

Sébastien Cevey

@theefer

http://slides.com/theefer/reactive-uis

Fully Reactive User Interfaces

By Sébastien Cevey

• 4,055