Bacon/Kefir+React+Atom+Lenses

For Concise, Reactive UI

Vesa Karvonen

Background

  • Feenix / OVP UI
    • CMS, Packaging, Live streams, ...
  • Constraints
    • JavaScript + React
    • Had a meeting and was suggested
      • Redux, FFUX
      • But was given free hands! \o/

History

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.

Desire

Avoid boilerplate and glue

Avoid all-or-nothing / lock-in

Prefer declarative

Avoid unnecessary encoding

Structural programming

Components plug-and-play

(Stole the image from a tweet.)

Fortunately

There is actually nothing new here.

We are just combining a few old things.

What is difficult in UI Programming?

Maintaining consistent state in the face of async inputs.

Our Approach

In order of importance:

  1. Specify dependent computations as observables.
  2. Embed observables directly into JSX.
  3. Store state in mutable observable atoms.
  4. Use lenses to selectively transmit state via atoms.

All optional!

And can mix with other React components.

Not Cool, but Calmm

Model is just JSON. Meta contains operations on JSON.

Examples

Libraries


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)
}
            

Atoms

Create new

Atom(vInitial)

For example:

const counter = Atom(0)

Inspect

atom.get()

For example:

counter.get() === 0

Mutate

atom.modify(vOld => vNew)

For example:

counter.modify(c => c + 1)

Now:

counter.get() === 1

Also for convenience:

atom.set(vNew) === atom.modify(() => vNew)

Atoms are first class objects

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.

Atoms are observable

(Technically Atoms are Properties, a subclass of Observables.)

Avoid get.

Use "FRP" combinators to express dependent computations.

Dependent Computations

Concepts

Observables

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.

Combining 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)
            

What do we gain?

Properties being observed are kept in sync.

We just decide which properties to observe.

Encoding mutation via streams...


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 programming

Don't use streams to encode simple synchronous properties.

Use streams to deal with...


const delayedCounter =
         increment.map(() => +1)
  .merge(decrement.map(() => -1).delay(100))
  .scan(0, (sum, delta) => sum + delta)
            

...asynchronous processes when you actually need them.

Embedding Observables into JSX

What happens?


const greetingsTarget = Atom("world")
...
<div>Hello, {greetingsTarget}!</div>
            

It crashes. React cannot render observables.

Lifted elements


const greetingsTarget = Atom("world")
...
<K.div>Hello, {greetingsTarget}!</K.div>
            
x

Continued


<K.input type="text"
         value={greetingsTarget}
         onChange={e => greetingsTarget.set(e.target.value)}/>
            
x

How?

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.

The class of a lifted element


componentWillMount() {
   ... subscribe ...
}

componentWillUnmount() {
   ... dispose ...
}

render() {
  return this.state.rendered
}
            

Subscribe creates a stream to update rendered state.

Performance?

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.

Performance (continued)

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.

Lenses

Lenses

Let you declare a path to an element.

  • That you can then use to
    • view, and
    • update
    the element.
  • Partial lenses also allow one to
    • insert, and
    • delete
    the element.

Example

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!

Example (continued)

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}]}
            

Example (continued)

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.

Partial lenses...

...allow one to, e.g.

  • Define default and required values.
  • Normalize data (e.g. sort items).
  • Filter from a list.
  • Augment data when viewed.
  • Choose item to view conditionally.
  • And more...

See documentation for more examples and details.

Atom also supports lenses


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")
              

Atoms + Lenses = Powertool

Allows your code to directly follow the structure

  • of the produced HTML view, and
  • of the underlying JSON model

to create a functioning, composable component.

Structural Programming

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?

Undo

Best practises

Use Atoms for simple data-binding

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.

Clearly separate meta model

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.

Don't overdo components

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?

What about...

Routing?

Http requests / IO?

Two-Way Binding

Issues raised in AngularJS: The Bad Parts:

Dynamic scoping and Parameter name based DI
We use ordinary JavaScript with lexical scoping.
Data is explicitly routed using lenses and properties.
The digest loop
Property updates are done incrementally by the underlying "FRP" library.
Redefining terminology
All concepts have been around for a long time (Observable, Property, Atom, Lens).

Questions?

Exercises

Prepare the project

Bacon based project

Kefir based project

Add unit tests for the meta-model

The meta model is just a bunch of operations on model.

Add e.g. mocha to the project and tycittele a few tests.

Add homepage -link field

Make editable link component.

Look at TextInput.

When not editing, render as a link.

Add birthday field

Select some React datepicker component.

Bind it to the model.

Add filtering

Add a text input for a search text.

Only show items containing matching text.

Add scrolling

Limit the number of simultaneously shown items.

Let the user scroll through the list.

Add local storage and Undo

For undo, you can use Atom.Undo.

For storage, you can use Atom.Storage.

Do you store history to local storage or not?

Add external storage

You will likely need to add Node (Express) to the project.