[ ≡ | Motivation | Tutorial | Reference | About ]
This library provides a family of concepts and tools for managing state with lenses and Kefir.
Atom(value)Atom()atom.get()atom.modify(currentValue => newValue)atom.set(value)atom.remove()atom.view(lens)holding(() => ...)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.
The rest of this README contains a tutorial to managing state using atoms and provides a reference manual for this library.
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.
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.
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.
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.
Then we write a higher-order component that can display a list of items:
const Items = ({items, Item}) => (
{U.mapElemsWithIds(
'id',
(item, key) => (
<Item {...{key, item}} />
),
items
)}
)
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.
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}) => (
<Remove removable={item} />
<Counter count={U.view(count, item)} />
{U.view('name', item)}
)
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
write far more interesting lenses.
We are nearly done! We just need one more component for products:
const count = item => [
L.find(R.whereEq({id: L.get('id', item)})),
L.defaults(item),
'count',
L.defaults(0),
L.normalize(R.max(0))
]
const ProductItem = cart => ({item}) => (
<Counter count={U.view(count(item), cart)} />
{U.view('name', item)}
)
The ProductItem component is also designed to work as an Item for the
previous Items component. Note that ProductItem actually takes two curried
arguments. The first argument cart is supposed to refer to cart state.
ProductItem also reuses the Counter component. This time we give it another
non-trivial lens. The count lens is a parameterized lens that is given an
item to put into the cart.
We now have all the components to put together our shopping cart application. Here is a list of some Finnish delicacies:
const productsData = [
{id: 1, name: 'Sinertävä lenkki 500g'},
{id: 2, name: 'Maksainen loota 400g'},
{id: 3, name: 'Maidon tapainen 0.9l'},
{id: 4, name: 'Festi moka kaffe 500g'},
{id: 5, name: 'Niin hyvää ettei 55g'},
{id: 6, name: 'Suklaa Nipponi 37g'}
]
And, finally, here is our Shop:
const Shop = ({cart, products}) => (
<h2>Products</h2>
<Items Item={ProductItem(cart)} items={products} />
<h2>Shopping Cart</h2>
<Items Item={CartItem} items={cart} />
)
The Shop above uses the higher-order Items component twice with different
Item components and different lists of items.
For the purposes of this example we are done. Here is a summary:
We wrote several components such as Counter, Remove and Items that are
not specific to the application in any way.
Each component is just one referentially transparent function that takes (possibly reactive variables as) parameters and returns VDOM.
We composed components together as VDOM expressions.
We used Counter and Items twice in different contexts.
When using Counter we used lenses to decompose application specific state to
match the interface of the component.
Typically one only uses the default export
import Atom from 'kefir.atom'
of this library. It provides a convenience function that constructs a new
instance of the Atom class.
Atom(value)Creates a new atom with the given initial value. For example:
const notEmpty = Atom('initial')
notEmpty.get()
// 'initial'
notEmpty.log()
// [property] <value:current> initial
Atom()Creates a new atom without an initial value. For example:
const empty = Atom()
empty.get()
// undefined
empty.log()
empty.set('first')
// [property] <value> first
atom.get()Synchronously computes the current value of the atom. For example:
const root = Atom({x: 1})
const x = root.view('x')
x.get()
// 1
Use of get is discouraged: prefer to depend on an atom as you would with
ordinary Kefir properties.
When get is called on an AbstractMutable that has
a root Atom that does not have a value, get returns the
values of those Atoms as undefined. For example:
const empty = Atom()
const notEmpty = Atom('initial')
const both = new Molecule({empty, notEmpty})
both.get()
// { empty: undefined, notEmpty: 'initial' }
atom.modify(currentValue => newValue)Conceptually applies the
$ claude mcp add kefir.atom \
-- python -m otcore.mcp_server <graph>