Kefir.atom
Composable and decomposable reactive state with lenses and Kefir
Install / Use
/learn @calmm-js/Kefir.atomREADME
[ ≡ | Motivation | Tutorial | Reference | About ]
This library provides a family of concepts and tools for managing state with lenses and Kefir.
Contents
Motivation
Use of state and mutation is often considered error-prone and rightly so. Stateful concepts are inherently difficult, because, unlike stateless concepts, they include the concept of time: state changes over time. When state changes, computations based on state, including copies of state, may become invalid or inconsistent.
Using this library:
-
You can store state in first-class objects called Atoms.
- This means that program components can declare the state they are interested in as parameters and share state by passing references to state as arguments without copying state.
-
You can declare decomposed first-class views of state using lenses and composed first-class views of state as Molecules.
- This means that program components can declare precisely the state they are interested in as parameters independently of the storage of state.
-
You get consistent read-write access to state using get and modify operations at any point and through all views.
- This means that by using views, both decomposed and composed, of state you can avoid copying state and the inconsistency problems associated with such copying.
-
You can declare arbitrary dependent computations using observable combinators from Kefir as AbstractMutables are also Kefir properties.
- This means that you can declare computations dependent upon state independently of time as such computation are kept consistent as state changes over time.
-
You can mutate state through multiple views and multiple atomic modify operations in a transactional manner by holding event propagation from state changes.
- This means that you can avoid some glitches and unnecessary computations of intermediate states.
-
You can avoid unnecessary recomputations, because program components can declare precisely the state they are interested in and views of state only propagate actual changes of state.
- This means that algorithmic efficiency is a feature of this library rather than an afterthought requiring further innovation.
The rest of this README contains a tutorial to managing state using atoms and provides a reference manual for this library.
Tutorial
Let's write the very beginnings of a Shopping Cart UI using atoms with the
karet and via the
karet.util libraries.
Karet is simple library that allows one to embed Kefir observables into React VDOM. If this tutorial advances at a too fast a pace, then you might want to read a longer introduction to the approach.
This example is actually a stripped down version of the Karet Shopping Cart example that you can see live here.
Counters are not toys!
So, how does one create a Shopping Cart UI?
Well, of course, the first thing is to write the classic counter component:
const Counter = ({count}) => (
<span>
<button onClick={U.doModify(count, R.dec)}>-</button>
{count}
<button onClick={U.doModify(count, R.inc)}>+</button>
</span>
)
The Counter component displays a count, which is supposed to refer to state
that contains an integer, and buttons labeled - and + that decrement and
increment the count using modify.
As you probably know, a counter component such as the above is a typical first example that the documentation of any respectable front-end framework will give you. Until now you may have mistakenly thought that those are just toys.
Component, remove thyself!
The next thing is to write a component that can remove itself:
const Remove = ({removable}) => (
<button onClick={U.doRemove(removable)}>x</button>
)
The Remove component gives you a button labeled x that calls
remove on the removable state given to it.
Pure and stateless
At this point it might be good idea to point out that both the previous
Counter component and the above Remove component are
referentially transparent
aka pure functions. Furthermore, instances of Counter and Remove are
stateless. This actually applies to all components in this tutorial and most
components in real-world Calmm applications can also be pure functions whose
instantiations are stateless. First-class, decomposable, and observable state
makes it easy to store state outside of components and make the components
themselves pure and stateless.
Lists are simple data structures
Then we write a higher-order component that can display a list of items:
const Items = ({items, Item}) => (
<div>
{U.mapElemsWithIds(
'id',
(item, key) => (
<Item {...{key, item}} />
),
items
)}
</div>
)
The Items component is given state named items that is supposed to refer to
an array of objects. From that array it then produces an unordered list of
Item components, passing them an item that corresponds to an element of the
items state array.
Items in a cart
We haven't actually written anything shopping cart specific yet. Let's change that by writing a component for cart items:
const count = [L.removable('count'), 'count', L.defaults(0)]
const CartItem = ({item}) => (
<div>
<Remove removable={item} />
<Counter count={U.view(count, item)} />
{U.view('name', item)}
</div>
)
The CartItem component is designed to work as Item for the previous Items
component. It is a simple component that is given state named item that is
supposed to refer to an object containing name and count fields. CartItem
uses the previously defined Remove and Counter components. The Remove
component is simply passed the item as the removable. The Counter
component is given a lensed
view of the count. The count lens makes it so that when the count
property reaches 0 the whole item is removed.
This is important: By using a simple lens as an adapter, we could
plug
the previously defined Counter component into the shopping cart state.
If this is the first time you encounter
partial lenses, then the
definition of count may be difficult to understand, but it is not very complex
at all. It works like this. It looks at the incoming object and grabs all the
properties as props. It then uses those to return a lens that, when written
through, will replace an object of the form {...props, count: 0} with
undefined. This way, when the count reaches 0, the whole item gets
removed. After working with partial lenses for some time you will be able to
w
