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) {
\$('.loading-rate').show();
lookupRate(currency).then(newRate => {
\$('.loading-rate').hide();
\$('.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\$,
rateLoading\$
};
}
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

loaded cells

tableHeight\$

itemCount\$

items\$

searchQuery\$

resizeEvents\$

windowWidth\$

columnCount\$

rangeToLoad\$

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,781