https://github.com/danr/reactive-lens
A lightweight library for pure, reactive and composable state.
https://github.com/danr/reactive-lens
Last synced: about 1 year ago
JSON representation
A lightweight library for pure, reactive and composable state.
- Host: GitHub
- URL: https://github.com/danr/reactive-lens
- Owner: danr
- License: mit
- Created: 2017-10-18T15:09:44.000Z (almost 9 years ago)
- Default Branch: master
- Last Pushed: 2018-03-12T10:54:50.000Z (over 8 years ago)
- Last Synced: 2025-05-29T16:47:49.514Z (about 1 year ago)
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/reactive-lens
- Size: 181 KB
- Stars: 15
- Watchers: 2
- Forks: 0
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# reactive-lens
> A lightweight library for pure, reactive and composable state.
## Synopsis
The `Store` in this library is a _reactive lens_: a partially applied, existentially quantified lens with a change listener.
```javascript
import { Store } from 'reactive-lens'
const increment = x => x + 1
const decrement = x => x - 1
const store = Store.init({left: 0, right: 0})
store.on(x => console.log(x))
store.at('left').modify(increment)
store.at('right').modify(increment)
store.at('left').modify(decrement)
```
Hooking it up with the DOM:
```typescript
import { Store } from 'reactive-lens'
const store = Store.init({left: '', right: ''})
function Input(store: Store) {
const input = document.createElement('input')
input.value = store.get()
store.on(x => input.value = x)
input.addEventListener('input', function () { store.set(this.value) })
}
const body = document.getElementsByTagName('body')[0]
body.appendChild(Input(store.at('left')))
body.appendChild(Input(store.at('right')))
```
## API overview
* class Store
* init
* get
* set
* update
* modify
* on
* ondiff
* transaction
* via
* at
* pick
* omit
* relabel
* merge
* arr
* each
* storage_connect
* location_connect
* attach
* interface Lens
* get
* set
* module Lens
* lens
* relabel
* at
* iso
* pick
* key
* def
* seq
* omit
* index
* module Undo
* undo
* redo
* advance
* init
* advance_to
* can_undo
* can_redo
* interface Undo
* now
* prev
* next
* interface Stack
* top
* pop
* module Requests
* request_maker
* request
* process_requests
* Diff
* Omit
## Documentation
### class Store
Store for some state
Store laws (assuming no listeners):
1. `s.set(a).get() = a`
2. `s.set(s.get()).get() = s.get()`
3. `s.set(a).set(b).get() = s.set(b).get()`
Store laws with listeners:
1. `s.transaction(() => s.set(a).get()) = a`
2. `s.transaction(() => s.set(s.get()).get()) = s.get()`
3. `s.transaction(() => s.set(a).set(b).get()) = s.set(b).get()`
A store is a partially applied, existentially quantified lens with a change listener.
* **init**: `(s0: S) => Store`
Make the root store (static method)
* **get**: `() => S`
Get the current value (which must not be mutated)
```typescript
const store = Store.init(1)
store.get()
// => 1
```
* **set**: `(s: S) => Store`
Set the value
```typescript
const store = Store.init(1)
store.set(2)
store.get()
// => 2
```
Returns itself.
* **update**: `(parts: { [k in K]: S[K]; }) => Store`
Update some parts of the state, keep the rest constant
```typescript
const store = Store.init({a: 1, b: 2})
store.update({a: 3})
store.get()
// => {a: 3, b: 2}
```
Returns itself.
* **modify**: `(f: (s: S) => S) => Store`
Modify the value in the store (must not use mutation: construct a new value)
```typescript
const store = Store.init(1)
store.modify(x => x + 1)
store.get()
// => 2
```
Returns itself.
* **on**: `(k: (s: S) => void) => () => void`
React on changes. Returns the unsubscribe function.
```typescript
const store = Store.init(1)
let last
const off = store.on(x => last = x)
store.set(2)
last // => 2
off()
store.set(3)
last // => 2
```
* **ondiff**: `(k: (new_value: S, old_value: S) => void) => () => void`
React on a difference in value. Returns the unsubscribe function.
```typescript
const store = Store.init({a: 0})
let diffs = 0
const off = store.ondiff((new_value, old) => {
assert.notEqual(new_value, old)
diffs++
})
diffs // => 0
const object = {a: 1}
store.set(object) // diff: new object
diffs // => 1
store.set(object) // no diff: same object
diffs // => 1
store.set({a: 2}) // diff: new object
diffs // => 2
store.set({a: 2}) // diff: this is yet another new literal object
diffs // => 3
store.set(store.get()) // no diff: same object
diffs // => 3
store.modify(x => x) // no diff: same object
diffs // => 3
store.at('a').modify(x => x) // diff: at does not see if the value actually changed
diffs // => 4
```
Note: keeps a reference to the last value in memory.
* **transaction**: `(m: () => A) => A`
Start a new transaction: listeners are only invoked when the
(top-level) transaction finishes, and not on set (and modify) inside the transaction.
```typescript
const store = Store.init(1)
let last
store.on(x => last = x)
store.transaction(() => {
store.set(2)
assert.equal(last, undefined)
return 3
}) // => 3
last // => 2
```
* **via**: `(lens: Lens) => Store`
Zoom in on a subpart of the store via a lens
```typescript
const store = Store.init({a: 1, b: 2} as Record)
const a_store = store.via(Lens.key('a'))
a_store.set(3)
store.get() // => {a: 3, b: 2}
a_store.get() // => 3
a_store.set(undefined)
store.get() // => {b: 2}
```
* **at**: `(k: K) => Store`
Make a substore at a key
```typescript
const store = Store.init({a: 1, b: 2})
store.at('a').set(3)
store.get() // => {a: 3, b: 2}
store.at('a').get() // => 3
```
Note: the key must always be present.
* **pick**: `(...ks: Array) => Store<{ [K in Ks]: S[K]; }>`
Make a substore by picking many keys
```typescript
const store = Store.init({a: 1, b: 2, c: 3})
store.pick('a', 'b').get() // => {a: 1, b: 2}
store.pick('a', 'b').set({a: 5, b: 4})
store.get() // => {a: 5, b: 4, c: 3}
```
Note: the keys must always be present.
* **omit**: `(...ks: Array) => Store>`
Make a substore which omits some keys
```typescript
const store = Store.init({a: 1, b: 2, c: 3, d: 4})
const cd = store.omit('a', 'b')
cd.get() // => {c: 3, d: 4}
cd.set({c: 5, d: 6})
store.get() // {a: 1, b: 2, c: 5, d: 6}
```
* **relabel**: `(stores: { [K in keyof T]: Store; }) => Store`
Make a substore by relabelling
```typescript
const store = Store.init({a: 1, b: 2, c: 3})
const other = store.relabel({x: store.at('a'), y: store.at('b')})
other.get() // => {x: 1, y: 2}
other.set({x: 5, y: 4})
store.get() // => {a: 5, b: 4, c: 3}
```
Note: must not use the same part of the store several times.
* **merge**: `(other: Store) => Store`
Merge two stores
```typescript
const store = Store.init({a: 1, b: 2, c: 3})
const small = store.pick('a')
const other = small.merge(store.relabel({z: store.at('c')}))
other.get() // => {a: 1, z: 3}
other.set({a: 0, z: 4})
store.get() // => {a: 0, b: 2, c: 4}
```
Note: the two stores must originate from the same root.
Note: this store and the other store must both be objects.
Note: must not use the same part of the store several times.
* **arr**: `(store: Store>, k: K) => Array[K]`
Set the value using an array method (purity is ensured because the spine is copied before running the function)
```typescript
const store = Store.init(['a', 'b', 'c', 'd'])
Store.arr(store, 'splice')(1, 2, 'x', 'y', 'z') // => ['b', 'c']
store.get() // => ['a', 'x', 'y', 'z', 'd']
```
(static method)
* **each**: `(store: Store>) => Array>`
Get partial stores for each position currently in the array
```typescript
const store = Store.init(['a', 'b', 'c'])
Store.each(store).map((substore, i) => substore.modify(s => s + i.toString()))
store.get() // => ['a0', 'b1', 'c2']
```
(static method)
Note: exceptions are thrown when looking outside the array.
* **storage_connect**: `(key?: string, audit?: (s: S) => boolean, api?: { get: (key: string) => string; set: (key: string, data: string) => void; }) => () => void`
Connect with local storage
* **location_connect**: `(to_hash: (state: S) => string, from_hash: (hash: string) => S, api?: { get(): string; set(hash: string): void; on(cb: () => void): void; }) => () => void`
Connect with window.location.hash
* **attach**: `(render: (vdom: VDOM) => void, init_state: S, setup_view: (store: Store) => () => VDOM) => (setup_next_view: (store: Store) => () => VDOM) => void`
Attach a store with a virtual DOM, returning the reattach function for hot module reloading.
### interface Lens
A lens: allows you to operate on a subpart `T` of some data `S`
Lenses must conform to these three lens laws:
`l.get(l.set(s, t)) = t`
`l.set(s, l.get(s)) = s`
`l.set(l.set(s, a), b) = l.set(s, b)`
* **get**: `(s: S) => T`
Get the value via the lens
* **set**: `(s: S, t: T) => S`
Set the value via the lens
### module Lens
Common lens constructors and functions
* **lens**: `(get: (s: S) => T, set: (s: S, t: T) => S) => Lens`
Make a lens from a getter and setter
Note: lenses are subject to the three lens laws
* **relabel**: `(lenses: { [K in keyof T]: Lens; }) => Lens`
Lens from a record of lenses
Note: must not use the same part of the store several times.
* **at**: `(k: K) => Lens`
Lens to a key in a record
Note: the key must always be present.
* **iso**: `(f: (s: S) => T, g: (t: T) => S) => Lens`
Make a lens from an isomorphism.
```typescript
const store = Store.init(5)
const doubled = store.via(Lens.iso(x => 2 * x, x => x / 2))
doubled.get() // => 10
doubled.set(50)
store.get() // => 25
doubled.modify(x => x * 2).get() // => 100
store.get() // => 50
```
Note: requires that for all `s` and `t` we have `f(g(t)) = t` and `g(f(s)) = s`
* **pick**: `(...keys: Array) => Lens`
Lens to a keys in a record
Note: the keys must always be present.
* **key**: `(k: K) => Lens`
Lens to a key in a record which may be missing
Note: setting the value to undefined removes the key from the record.
* **def**: `(missing: A) => Lens`
Lens which refer to a default value instead of undefined
```typescript
const store = Store.init({a: 1, b: 2} as Record)
const a_store = store.via(Lens.key('a')).via(Lens.def(0))
a_store.set(3)
store.get() // => {a: 3, b: 2}
a_store.get() // => 3
a_store.set(0)
store.get() // => {b: 2}
a_store.modify(x => x + 1)
store.get() // => {a: 1, b: 2}
```
* **seq**: `(lens1: Lens, lens2: Lens) => Lens`
Compose two lenses sequentially
* **omit**: `(...ks: Array) => Lens>`
Make a lens which omits some keys
* **index**: `(i: number) => Lens, A>`
Partial lens to a particular index in an array
```typescript
const store = Store.init([0, 1, 2, 3])
const first = store.via(Lens.index(0))
first.get() // => 0
first.set(99)
store.get() // => [99, 1, 2, 3]
```
Note: an exception is thrown if you look outside the array.
### module Undo
History zipper functions
```typescript
const {undo, redo, advance, advance_to} = Undo
const store = Store.init(Undo.init({a: 1, b: 2}))
const modify = op => store.modify(op)
const now = store.at('now')
now.get() // => {a: 1, b: 2}
modify(advance_to({a: 3, b: 4}))
now.get() // => {a: 3, b: 4}
modify(undo)
now.get() // => {a: 1, b: 2}
modify(redo)
now.get() // => {a: 3, b: 4}
modify(advance)
now.update({a: 5})
now.get() // => {a: 5, b: 4}
modify(undo)
now.get() // => {a: 3, b: 4}
modify(undo)
now.get() // => {a: 1, b: 2}
modify(undo)
now.get() // => {a: 1, b: 2}
```
* **undo**: `(h: Undo) => Undo`
Undo iff there is a past
* **redo**: `(h: Undo) => Undo`
Redo iff there is a future
* **advance**: `(h: Undo) => Undo`
Advances the history by copying the present state
* **init**: `(now: S) => Undo`
Initialise the history
* **advance_to**: `(s: S) => (h: Undo) => Undo`
Advances the history to some new state
* **can_undo**: `(h: Undo) => boolean`
Is there a state to undo to?
* **can_redo**: `(h: Undo) => boolean`
Is there a state to redo to?
### interface Undo
History zipper
* **now**: `S`
* **prev**: `Stack`
* **next**: `Stack`
### interface Stack
A non-empty stack
* **top**: `S`
* **pop**: `Stack`
### module Requests
Utility functions to make Elm/Redux-style requests
A queue of requests are maintained in an array.
TODO: Document and test.
* **request_maker**: `(store: Store>) => (request: R) => void`
Make a function for making requests
* **request**: `(store: Store>, request: R) => void`
Make a request
* **process_requests**: `(store: Store>, process: (request: R) => void) => () => void`
Process requests, one at a time
Retuns the off function.
* **Diff**: `undefined`
* **Omit**: `undefined`