Got empty project from Matti Lankinen.
Matti had just noticed perf problems with megablob.
I had just used Reagent in production.
Refactoring Matti's project (createStore) created Atom.
And with Matti developed ways to embed Bacon into JSX.
Later started using Bacon Model and, thanks to it, lenses.
Realized Bacon Model creates cycles => Bacon Atom.
And then developed Partial Lenses library.
Avoid boilerplate and glue
Avoid all-or-nothing / lock-in
Prefer declarative
Avoid unnecessary encoding
Structural programming
Components plug-and-play
There is actually nothing new here.
We are just combining a few old things.
Maintaining consistent state in the face of async inputs.
In order of importance:
All optional!
And can mix with other React components.
Model is just JSON. Meta contains operations on JSON.
import Atom from "kefir.atom"
import K,{bind, classes, fromIds, fromKefir} from "kefir.react.html"
import Kefir from "kefir"
or
import Atom from "bacon.atom"
import B,{bind, classes, fromIds, fromBacon} from "bacon.react.html"
import Bacon from "baconjs"
and
import L from "partial.lenses"
import R from "ramda"
<Clock/>
const Clock = () => <K.div>
{K(oncePerSec, () => new Date().toString())}
</K.div>
const oncePerSec = Kefir.constant().merge(Kefir.interval(1000))
<InputAdd/>
const InputAdd = ({elems = Atom([]), entry = Atom("")}) => <div>
<div>
<K.input type="text" {...bind({value: entry})}/>
<button onClick={() => {const elem = entry.get().trim()
if (elem) {
elems.modify(R.append(elem))
entry.set("")}}}>Add</button>
</div>
<K.ul>
{K(elems, es => es.map((e, i) => <li key={i}>{e}</li>))}
</K.ul>
</div>
<BMI/>
const BMI = ({bmi = Atom(M.mock)}) =>
<Augmented bmi={bmi.lens(M.BMI.augment)}/>
const Augmented = ({bmi}) =>
<K.div {...classes("bmi", K(bmi, M.BMI.classification))}>
<Slider title="Weight" units="kg" min={40} max={140}
value={bmi.lens(M.BMI.weight)}/>
<Slider title="Height" units="cm" min={140} max={210}
value={bmi.lens(M.BMI.height)}/>
<div>BMI: <K.span className="bmi-value">
{K(bmi, M.BMI.bmi)}</K.span></div>
</K.div>
const Slider = ({title, units, value, ...props}) => <div>
<K.div>{title}: {value}{units}</K.div>
<K.input type="range" {...bind({value})} {...props}/>
</div>
export const BMI = {
augment: L.augment({
bmi: ({height, weight}) =>
Math.round(weight/(height * height * 0.0001))}),
bmi: R.prop("bmi"),
height: "height",
weight: "weight",
classification: ({bmi}) =>
bmi < 15 ? "bmi-underweight bmi-underweight-severely"
: bmi < 18.5 ? "bmi-underweight"
: bmi < 25 ? "bmi-normal"
: bmi < 30 ? "bmi-overweight"
: bmi < 35 ? "bmi-obese"
: bmi < 40 ? "bmi-obese bmi-obese-severely"
: "bmi-obese bmi-obese-very"
}
<Phonebook/>
const Phonebook = ({phonebook: pb = Atom(M.mock)}) => <div>
<button onClick={() => pb.modify(M.Phonebook.addContact())}>
New</button>
<Contacts contacts={pb.lens(M.Phonebook.contacts)}/>
</div>
const Contacts = ({contacts}) => <K.div>
{fromIds(K(contacts, M.Contacts.indices), i =>
<Contact key={i} contact={contacts.lens(i)}/>)}
</K.div>
const Contact = ({contact}) => <div>
<TextInput value={contact.lens(M.Contact.name)}/>
<TextInput value={contact.lens(M.Contact.number)}/>
<button onClick={() => contact.modify(M.Contact.remove)}>
Remove</button>
</div>
const TextInput = ({value = Atom("")}) => {
const editing = Atom(false)
const exit = () => editing.set(false)
const save = e => {value.set(e.target.value); exit(e)}
return fromKefir(K(editing, e => e
? <K.input key="1" type="text" autoFocus
defaultValue={value}
onFocus={({target: t}) =>
t.selectionStart = t.value.length}
onKeyDown={({key}) => key === "Enter" && save(e)
|| key === "Escape" && exit(e)}
onBlur={save}/>
: <K.input key="0" type="text" disabled
{...{value}}
onDoubleClick={() => editing.set(true)}/>))
}
export const mock =
[{name: "Mr Digits", number: "1-23-456789"}]
export const Contact = {
create: ({name = "", number = ""} = {}) => ({name, number}),
remove: () => {},
id: "id",
name: "name",
number: "number"
}
export const Contacts = {
indices: R.pipe(R.length, R.range(0))
}
export const Phonebook = {
contacts: L.define([]),
addContact: R.pipe(Contact.create, R.append)
}
Atom(vInitial)
For example:
const counter = Atom(0)
atom.get()
For example:
counter.get() === 0
atom.modify(vOld => vNew)
For example:
counter.modify(c => c + 1)
Now:
counter.get() === 1
Also for convenience:
atom.set(vNew) === atom.modify(() => vNew)
Can store in data structures, pass to and return from functions.
Want multiple things to always share the same value?
Put the value in an Atom and share it.
Avoid get
.
Use "FRP" combinators to express dependent computations.
Stream
A B CD E F G HIJ K L M N O P QR S
e.g. key down events, mouse clicks, ...
filter, skip, merge, combine, scan, toProperty, ...
Property
AABBCDDDDDDDDEEFFFFFFFFFFFFFFGGGGGGGHIJJKKLKKKMMMNNNNOOOPPPPPPQRRRRS
e.g. text accumulated, position of pointer, ...
combine, sample, changes, toEventStream, ...
Both are used. We are mostly concerned with properties.
Bacon.combineWith(p1, ..., pN, (v1, ..., vN) => result)
Kefir.combine(p1, ..., pN, (v1, ..., vN) => result)
We abbreviate:
B(p1, ..., pN, (v1, ..., vN) => result)
K(p1, ..., pN, (v1, ..., vN) => result)
For example:
const input = Atom("This is an example.")
const words = K(input, R.split(" "))
const numWords = K(words, R.length)
const unique = K(words, R.uniqBy(R.toUpper))
const numUnique = K(unique, R.length)
Properties being observed are kept in sync.
We just decide which properties to observe.
const counter = increment.map(() => +1)
.merge(decrement.map(() => -1))
.scan(0, (sum, delta) => sum + delta)
...became fashionable when reactive became popular.
What do you gain by that?
Typically not a thing.
Cargo cult programmingDon't use streams to encode simple synchronous properties.
const delayedCounter =
increment.map(() => +1)
.merge(decrement.map(() => -1).delay(100))
.scan(0, (sum, delta) => sum + delta)
...asynchronous processes when you actually need them.
const greetingsTarget = Atom("world")
...
<div>Hello, {greetingsTarget}!</div>
It crashes. React cannot render observables.
const greetingsTarget = Atom("world")
...
<K.div>Hello, {greetingsTarget}!</K.div>
<K.input type="text"
value={greetingsTarget}
onChange={e => greetingsTarget.set(e.target.value)}/>
JSX, e.g.
<div someProp="a value"><p>Child</p></div>
evaluates into a tree of objects, roughly
{ "type": "div",
"props":
{ "someProp": "a value",
"children":
{ "type": "p",
"props": { "children": "Child" } } } }
and the props are passed to the React class.
componentWillMount() {
... subscribe ...
}
componentWillUnmount() {
... dispose ...
}
render() {
return this.state.rendered
}
Subscribe creates a stream to update rendered
state.
Everything can be made to work incrementally.
Mount and unmount take a little extra.
But then you only recompute changed VDOM.
Asymptotically better than recomputing all VDOM.
Never write another React class or shouldComponentUpdate.
Properties and streams everywhere.
Possibly thousands upon thousands.
Bacon is neither space nor time optimal. :(
Kefir uses significantly less (~5x).
With Kefir performance seems very competitive.
Let you declare a path to an element.
Given:
const data = {items: [{id: "a", value: 20}, {id: "b", value: 10}]}
We could write a parameterized lens to access values:
const valueOf = id => L("items",
L.required([]),
L.find(R.whereEq({id})),
L.default({id}),
"value")
Let's take a moment to read each line above!
We can now view values:
> L.get(valueOf("a"), data)
20
And we can set values:
> L.set(valueOf("a"), 15, data)
{items: [{id: "a", value: 15}, {id: "b", value: 10}]}
And we can delete values:
> L.delete(valueOf("a"), data)
{items: [{id: "b", value: 10}]}
And we can insert values:
> L.set(valueOf("c"), 15, data)
{items: [{id: "a", value: 20},
{id: "b", value: 10},
{id: "c", value: 15}]}
Including the whole item.
...allow one to, e.g.
See documentation for more examples and details.
atom.lens(partial-lens)
For example:
const names = Atom(["first"])
const first = names.lens(L.index(0))
Now you can, e.g.
first.set("Still first")
Allows your code to directly follow the structure
to create a functioning, composable component.
The JSON model structure is unique.
The HTML view structure is unique.
Write down the structure of the problem and you have a solution.
Does your UI framework make you invent new structure?
Don't overuse Atoms: leads to imperative spaghetti.
Setting the value of an atom in response to a change of an atom is a smell.
Remember: You can use other kinds of wiring!
But more complex wiring seems to be rarely needed.
Atoms and Observable embedding make it easy to have: component = 1 function.
But you really dont want to clump everything together.
Separate the model, meta-model, control.
It makes code conceptually clearer.
It makes the models more easily testable and usable.
It is easy to go overboard with components.
Wrapping basic HTML elements, e.g. textarea or select, as components.
You end up making them difficult to customize.
Your components should do something substantial.
Does it have a non-trivial model?
Is it a combination of elements you use in lots of places?
Issues raised in AngularJS: The Bad Parts:
The meta model is just a bunch of operations on model.
Add e.g. mocha to the project and tycittele a few tests.
Make editable link component.
Look at TextInput.
When not editing, render as a link.
Select some React datepicker component.
Bind it to the model.
Add a text input for a search text.
Only show items containing matching text.
Limit the number of simultaneously shown items.
Let the user scroll through the list.
For undo, you can use Atom.Undo.
For storage, you can use Atom.Storage.
Do you store history to local storage or not?
You will likely need to add Node (Express) to the project.