CYCLE STATE MADE EASY
Me
A cyclist
STATE MANAGEMENT IN CYCLE
The Classical Way
Intent-Model-View
function Counter({DOM, initial$ = O.just(0)}) {
const mod$ = O.merge(
DOM.select(".dec").events("click").map(ev => state => state - 1),
DOM.select(".inc").events("click").map(ev => state => state + 1)
)
const count$ = initial$.first().concat(mod$).scan((state, mod) => mod(state)).shareReplay(1)
return {
DOM: count$.map(count =>
h("div", [
h("button.inc", "++"),
h("button.dec", "--"),
h("p", "Counter: " + count)
])
),
value$: count$
}
}
Intent
Model
View
-
Intent first (no dependecies)
-
Model depends on Intent
-
View depends on Model
But what if Intents depend on Model?
a.k.a. "advanced" (=real life) example
function main({DOM}) {
const stateModProxy$ = new Rx.Subject()
const state$ = stateModProxy$
.startWith({a: 0, b: 0})
.scan((state, mod) => mod(state))
.shareReplay(1)
const a = state$.map(s => isolate(Counter, "a")({DOM, initial$: O.just(s.a)})).shareReplay(1)
const b = state$.map(s => isolate(Counter, "b")({DOM, initial$: O.just(s.b)})).shareReplay(1)
const resetMod$ = DOM.select(".reset").events("click").map(() => () => ({a: 0, b: 0}))
const aChangeMod$ = a.map(s => s.value$.skip(1).first()).switch()
.map(val => state => ({...state, a: val}))
const bChangeMod$ = b.map(s => s.value$.skip(1).first()).switch()
.map(val => state => ({...state, b: val}))
const vdom$ = O.combineLatest(state$,
a.map(s => s.DOM).switch(),
b.map(s => s.DOM).switch(),
(state, a, b) =>
h("div", [
a, b,
h("hr"),
h("h2", `Total: ${state.a + state.b}`),
h("button.reset", "Reset")
]))
O.merge(resetMod$, aChangeMod$, bChangeMod$).subscribe(stateModProxy$)
return {DOM: vdom$}
}
POTENTIAL MEMORY LEAK!!
Intent
Model
View
"But it does not have to be like that"
State modification is a side effect
Let's put the state to a driver!
The Stanga way
Model-View-Intent
import {Model} from "stanga"
run(main, {
M: Model(0),
DOM: ...
})
function main({DOM, M}) {
const state$ = M
const vdom$ = state$.map(counter => h("div", [
h("h1", `Counter value is ${counter}`),
h("button.inc", "++"),
h("button.dec", "--")
]))
const mod$ = O.merge(
DOM.select(".inc").events("click").map(() => state => state + 1)
DOM.select(".dec").events("click").map(() => state => state - 1)
)
return {
DOM: vdom$,
M: M.mod(mod$)
}
}
Intent
Model
View
-
Model first, state comes from driver
-
View depends on Model
-
Intent returns mods to driver via sink
import {Model} from "stanga"
run(main, {
M: Model({a: 0, b: 0}),
DOM: ...
})
function main({DOM, M}) {
const state$ = M
const a$ = state$.lens("a")
const b$ = state$.lens("b")
const a = isolate(Counter)({DOM, M: a$})
const b = isolate(Counter)({DOM, M: b$})
const vdom$ = O.combineLatest(state$, a.DOM, b.DOM, (state, a, b) =>
h("div", [
a, b,
h("hr"),
h("h2", `Total: ${state.a + state.b}`),
h("button.reset", "Reset")
]))
const mod$ = O.merge(
DOM.select(".reset").events("click").map(() => () => ({a: 0, b: 0})),
a.M,
b.M
)
return {
DOM: vdom$,
M: mod$
}
}
Intent
Model
View
typeof a$ === typeof M
Some cool stuff MVI+lenses enable
Truly composable components by default
Advanced list processing
export default function main({DOM, M}) {
const counters$ = M
const childSinks$ = counters$.liftListById((id, counter$) =>
isolate(Counter, `counter-${id}`)({DOM, M: counter$.lens("val")}))
const childVTrees$ = flatCombine(childSinks$, "DOM").DOM
const vdom$ = O.combineLatest(counters$, childVTrees$, (counters, children) =>
h("div", [
h("ul", children.map(child => h("li", [child]))),
h("hr"),
h("h2", `Avg: ${avg(counters.map(c => c.val)).toFixed(2)}`),
h("button.reset", "Reset"),
h("button.add", "Add counter")
]))
const childMods$ = flatMerge(childSinks$, "M").M
const resetMod$ = DOM.select(".reset")
.events("click")
.map(() => counters => counters.map(c => ({...c, val: 0})))
const appendMod$ = DOM.select(".add")
.events("click")
.map(() => counters => [...counters, {id: nextId(), val: 0}])
return {
DOM: vdom$,
M: O.merge(M.mod(resetMod$), M.mod(appendMod$), childMods$)
}
}
Simple state pre-/post-processing
export default function main({DOM, M}) {
const {value$: counters$, canUndo$, canRedo$, mods: {undo$, redo$}} = undoable(M)
const list = CounterList({DOM, M: counters$})
const vdom$ = O.combineLatest(list.DOM, canUndo$, canRedo$,
(listVDom, canUndo, canRedo) =>
h("div", [
h("button.undo", {disabled: !canUndo}, "Undo"),
h("button.redo", {disabled: !canRedo}, "Redo"),
h("hr"),
listVDom
]))
const mod$ = O.merge(
undo$.sample(DOM.select(".undo").events("click")),
redo$.sample(DOM.select(".redo").events("click"))
)
return {
DOM: vdom$,
M: O.merge(mod$, list.M)
}
}
What next?
Thanks!
stanga
By Matti Lankinen
stanga
- 1,020