{"id":21467926,"url":"https://github.com/danr/reactive-lens","last_synced_at":"2025-07-15T05:32:11.029Z","repository":{"id":57331165,"uuid":"107424975","full_name":"danr/reactive-lens","owner":"danr","description":"A lightweight library for pure, reactive and composable state.","archived":false,"fork":false,"pushed_at":"2018-03-12T10:54:50.000Z","size":185,"stargazers_count":15,"open_issues_count":3,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-05-29T16:47:49.514Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/reactive-lens","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/danr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-10-18T15:09:44.000Z","updated_at":"2024-02-16T16:59:54.000Z","dependencies_parsed_at":"2022-09-07T16:30:22.993Z","dependency_job_id":null,"html_url":"https://github.com/danr/reactive-lens","commit_stats":null,"previous_names":["danr/projective"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/danr/reactive-lens","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danr%2Freactive-lens","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danr%2Freactive-lens/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danr%2Freactive-lens/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danr%2Freactive-lens/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/danr","download_url":"https://codeload.github.com/danr/reactive-lens/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danr%2Freactive-lens/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263687151,"owners_count":23496088,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-23T08:20:39.441Z","updated_at":"2025-07-15T05:32:10.683Z","avatar_url":"https://github.com/danr.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# reactive-lens\n\n\u003e A lightweight library for pure, reactive and composable state.\n\n## Synopsis\n\nThe `Store` in this library is a _reactive lens_: a partially applied, existentially quantified lens with a change listener.\n\n```javascript\nimport { Store } from 'reactive-lens'\n\nconst increment = x =\u003e x + 1\nconst decrement = x =\u003e x - 1\n\nconst store = Store.init({left: 0, right: 0})\n\nstore.on(x =\u003e console.log(x))\n\nstore.at('left').modify(increment)\nstore.at('right').modify(increment)\nstore.at('left').modify(decrement)\n```\n\nHooking it up with the DOM:\n\n```typescript\nimport { Store } from 'reactive-lens'\n\nconst store = Store.init({left: '', right: ''})\n\nfunction Input(store: Store\u003cstring\u003e) {\n  const input = document.createElement('input')\n  input.value = store.get()\n  store.on(x =\u003e input.value = x)\n  input.addEventListener('input', function () { store.set(this.value) })\n}\n\nconst body = document.getElementsByTagName('body')[0]\nbody.appendChild(Input(store.at('left')))\nbody.appendChild(Input(store.at('right')))\n```\n\n## API overview\n* class Store\n  * init\n  * get\n  * set\n  * update\n  * modify\n  * on\n  * ondiff\n  * transaction\n  * via\n  * at\n  * pick\n  * omit\n  * relabel\n  * merge\n  * arr\n  * each\n  * storage_connect\n  * location_connect\n* attach\n* interface Lens\n  * get\n  * set\n* module Lens\n  * lens\n  * relabel\n  * at\n  * iso\n  * pick\n  * key\n  * def\n  * seq\n  * omit\n  * index\n* module Undo\n  * undo\n  * redo\n  * advance\n  * init\n  * advance_to\n  * can_undo\n  * can_redo\n* interface Undo\n  * now\n  * prev\n  * next\n* interface Stack\n  * top\n  * pop\n* module Requests\n  * request_maker\n  * request\n  * process_requests\n* Diff\n* Omit\n\n## Documentation\n### class Store\n\nStore for some state\n\nStore laws (assuming no listeners):\n\n1. `s.set(a).get() = a`\n\n2. `s.set(s.get()).get() = s.get()`\n\n3. `s.set(a).set(b).get() = s.set(b).get()`\n\nStore laws with listeners:\n\n1. `s.transaction(() =\u003e s.set(a).get()) = a`\n\n2. `s.transaction(() =\u003e s.set(s.get()).get()) = s.get()`\n\n3. `s.transaction(() =\u003e s.set(a).set(b).get()) = s.set(b).get()`\n\nA store is a partially applied, existentially quantified lens with a change listener.\n* **init**: `\u003cS\u003e(s0: S) =\u003e Store\u003cS\u003e`\n\n  Make the root store (static method) \n* **get**: `() =\u003e S`\n\n  Get the current value (which must not be mutated)\n\n  ```typescript\n  const store = Store.init(1)\n  store.get()\n  // =\u003e 1\n  ```\n* **set**: `(s: S) =\u003e Store\u003cS\u003e`\n\n  Set the value\n\n  ```typescript\n  const store = Store.init(1)\n  store.set(2)\n  store.get()\n  // =\u003e 2\n  ```\n\n  Returns itself. \n* **update**: `\u003cK extends keyof S\u003e(parts: { [k in K]: S[K]; }) =\u003e Store\u003cS\u003e`\n\n  Update some parts of the state, keep the rest constant\n\n  ```typescript\n  const store = Store.init({a: 1, b: 2})\n  store.update({a: 3})\n  store.get()\n  // =\u003e {a: 3, b: 2}\n  ```\n\n  Returns itself. \n* **modify**: `(f: (s: S) =\u003e S) =\u003e Store\u003cS\u003e`\n\n  Modify the value in the store (must not use mutation: construct a new value)\n\n  ```typescript\n  const store = Store.init(1)\n  store.modify(x =\u003e x + 1)\n  store.get()\n  // =\u003e 2\n  ```\n\n  Returns itself. \n* **on**: `(k: (s: S) =\u003e void) =\u003e () =\u003e void`\n\n  React on changes. Returns the unsubscribe function.\n\n  ```typescript\n  const store = Store.init(1)\n  let last\n  const off = store.on(x =\u003e last = x)\n  store.set(2)\n  last // =\u003e 2\n  off()\n  store.set(3)\n  last // =\u003e 2\n  ```\n* **ondiff**: `(k: (new_value: S, old_value: S) =\u003e void) =\u003e () =\u003e void`\n\n  React on a difference in value. Returns the unsubscribe function.\n\n  ```typescript\n    const store = Store.init({a: 0})\n    let diffs = 0\n    const off = store.ondiff((new_value, old) =\u003e {\n      assert.notEqual(new_value, old)\n      diffs++\n    })\n    diffs // =\u003e 0\n    const object = {a: 1}\n    store.set(object)            // diff: new object\n    diffs                        // =\u003e 1\n    store.set(object)            // no diff: same object\n    diffs                        // =\u003e 1\n    store.set({a: 2})            // diff: new object\n    diffs                        // =\u003e 2\n    store.set({a: 2})            // diff: this is yet another new literal object\n    diffs                        // =\u003e 3\n    store.set(store.get())       // no diff: same object\n    diffs                        // =\u003e 3\n    store.modify(x =\u003e x)         // no diff: same object\n    diffs                        // =\u003e 3\n    store.at('a').modify(x =\u003e x) // diff: at does not see if the value actually changed\n    diffs                        // =\u003e 4\n  ```\n\n  Note: keeps a reference to the last value in memory. \n* **transaction**: `\u003cA\u003e(m: () =\u003e A) =\u003e A`\n\n  Start a new transaction: listeners are only invoked when the\n  (top-level) transaction finishes, and not on set (and modify) inside the transaction.\n\n  ```typescript\n  const store = Store.init(1)\n  let last\n  store.on(x =\u003e last = x)\n  store.transaction(() =\u003e {\n    store.set(2)\n    assert.equal(last, undefined)\n    return 3\n  })   // =\u003e 3\n  last // =\u003e 2\n  ```\n* **via**: `\u003cT\u003e(lens: Lens\u003cS, T\u003e) =\u003e Store\u003cT\u003e`\n\n  Zoom in on a subpart of the store via a lens\n\n  ```typescript\n  const store = Store.init({a: 1, b: 2} as Record\u003cstring, number\u003e)\n  const a_store = store.via(Lens.key('a'))\n  a_store.set(3)\n  store.get() // =\u003e {a: 3, b: 2}\n  a_store.get() // =\u003e 3\n  a_store.set(undefined)\n  store.get() // =\u003e {b: 2}\n  ```\n* **at**: `\u003cK extends keyof S\u003e(k: K) =\u003e Store\u003cS[K]\u003e`\n\n  Make a substore at a key\n\n  ```typescript\n  const store = Store.init({a: 1, b: 2})\n  store.at('a').set(3)\n  store.get() // =\u003e {a: 3, b: 2}\n  store.at('a').get() // =\u003e 3\n  ```\n\n  Note: the key must always be present. \n* **pick**: `\u003cKs extends keyof S\u003e(...ks: Array\u003cKs\u003e) =\u003e Store\u003c{ [K in Ks]: S[K]; }\u003e`\n\n  Make a substore by picking many keys\n\n  ```typescript\n  const store = Store.init({a: 1, b: 2, c: 3})\n  store.pick('a', 'b').get() // =\u003e {a: 1, b: 2}\n  store.pick('a', 'b').set({a: 5, b: 4})\n  store.get() // =\u003e {a: 5, b: 4, c: 3}\n  ```\n\n  Note: the keys must always be present. \n* **omit**: `\u003cK extends keyof S\u003e(...ks: Array\u003cK\u003e) =\u003e Store\u003cOmit\u003cS, K\u003e\u003e`\n\n  Make a substore which omits some keys\n\n  ```typescript\n  const store = Store.init({a: 1, b: 2, c: 3, d: 4})\n  const cd = store.omit('a', 'b')\n  cd.get() // =\u003e {c: 3, d: 4}\n  cd.set({c: 5, d: 6})\n  store.get() // {a: 1, b: 2, c: 5, d: 6}\n  ```\n* **relabel**: `\u003cT\u003e(stores: { [K in keyof T]: Store\u003cT[K]\u003e; }) =\u003e Store\u003cT\u003e`\n\n  Make a substore by relabelling\n\n  ```typescript\n  const store = Store.init({a: 1, b: 2, c: 3})\n  const other = store.relabel({x: store.at('a'), y: store.at('b')})\n  other.get() // =\u003e {x: 1, y: 2}\n  other.set({x: 5, y: 4})\n  store.get() // =\u003e {a: 5, b: 4, c: 3}\n  ```\n\n  Note: must not use the same part of the store several times. \n* **merge**: `\u003cT\u003e(other: Store\u003cT\u003e) =\u003e Store\u003cS \u0026 T\u003e`\n\n  Merge two stores\n\n  ```typescript\n  const store = Store.init({a: 1, b: 2, c: 3})\n  const small = store.pick('a')\n  const other = small.merge(store.relabel({z: store.at('c')}))\n  other.get() // =\u003e {a: 1, z: 3}\n  other.set({a: 0, z: 4})\n  store.get() // =\u003e {a: 0, b: 2, c: 4}\n  ```\n\n  Note: the two stores must originate from the same root.\n  Note: this store and the other store must both be objects.\n  Note: must not use the same part of the store several times. \n* **arr**: `\u003cA, K extends \"length\" | \"toString\" | \"toLocaleString\" | \"push\" | \"pop\" | \"concat\" | \"join\" | \"reverse\" | \"shift\" | \"slice\" | \"sort\" | \"splice\" | \"unshift\" | \"indexOf\" | \"lastIndexOf\" | \"every\" | \"some\" | \"forEach\" | \"map\" | \"filter\" | \"reduce\" | \"reduceRight\"\u003e(store: Store\u003cArray\u003cA\u003e\u003e, k: K) =\u003e Array\u003cA\u003e[K]`\n\n  Set the value using an array method (purity is ensured because the spine is copied before running the function)\n\n  ```typescript\n  const store = Store.init(['a', 'b', 'c', 'd'])\n  Store.arr(store, 'splice')(1, 2, 'x', 'y', 'z') // =\u003e ['b', 'c']\n  store.get() // =\u003e ['a', 'x', 'y', 'z', 'd']\n  ```\n\n  (static method) \n* **each**: `\u003cA\u003e(store: Store\u003cArray\u003cA\u003e\u003e) =\u003e Array\u003cStore\u003cA\u003e\u003e`\n\n  Get partial stores for each position currently in the array\n\n  ```typescript\n  const store = Store.init(['a', 'b', 'c'])\n  Store.each(store).map((substore, i) =\u003e substore.modify(s =\u003e s + i.toString()))\n  store.get() // =\u003e ['a0', 'b1', 'c2']\n  ```\n\n  (static method)\n\n  Note: exceptions are thrown when looking outside the array. \n* **storage_connect**: `(key?: string, audit?: (s: S) =\u003e boolean, api?: { get: (key: string) =\u003e string; set: (key: string, data: string) =\u003e void; }) =\u003e () =\u003e void`\n\n  Connect with local storage \n* **location_connect**: `(to_hash: (state: S) =\u003e string, from_hash: (hash: string) =\u003e S, api?: { get(): string; set(hash: string): void; on(cb: () =\u003e void): void; }) =\u003e () =\u003e void`\n\n  Connect with window.location.hash \n* **attach**: `\u003cS, VDOM\u003e(render: (vdom: VDOM) =\u003e void, init_state: S, setup_view: (store: Store\u003cS\u003e) =\u003e () =\u003e VDOM) =\u003e (setup_next_view: (store: Store\u003cS\u003e) =\u003e () =\u003e VDOM) =\u003e void`\n\n  Attach a store with a virtual DOM, returning the reattach function for hot module reloading. \n### interface Lens\n\nA lens: allows you to operate on a subpart `T` of some data `S`\n\nLenses must conform to these three lens laws:\n\n`l.get(l.set(s, t)) = t`\n\n`l.set(s, l.get(s)) = s`\n\n`l.set(l.set(s, a), b) = l.set(s, b)`\n* **get**: `(s: S) =\u003e T`\n\n  Get the value via the lens \n* **set**: `(s: S, t: T) =\u003e S`\n\n  Set the value via the lens \n### module Lens\n\nCommon lens constructors and functions \n* **lens**: `\u003cS, T\u003e(get: (s: S) =\u003e T, set: (s: S, t: T) =\u003e S) =\u003e Lens\u003cS, T\u003e`\n\n  Make a lens from a getter and setter\n\n  Note: lenses are subject to the three lens laws \n* **relabel**: `\u003cS, T\u003e(lenses: { [K in keyof T]: Lens\u003cS, T[K]\u003e; }) =\u003e Lens\u003cS, T\u003e`\n\n  Lens from a record of lenses\n\n  Note: must not use the same part of the store several times. \n* **at**: `\u003cS, K extends keyof S\u003e(k: K) =\u003e Lens\u003cS, S[K]\u003e`\n\n  Lens to a key in a record\n\n  Note: the key must always be present. \n* **iso**: `\u003cS, T\u003e(f: (s: S) =\u003e T, g: (t: T) =\u003e S) =\u003e Lens\u003cS, T\u003e`\n\n  Make a lens from an isomorphism.\n\n  ```typescript\n  const store = Store.init(5)\n  const doubled = store.via(Lens.iso(x =\u003e 2 * x, x =\u003e x / 2))\n  doubled.get() // =\u003e 10\n  doubled.set(50)\n  store.get() // =\u003e 25\n  doubled.modify(x =\u003e x * 2).get() // =\u003e 100\n  store.get() // =\u003e 50\n  ```\n\n  Note: requires that for all `s` and `t` we have `f(g(t)) = t` and `g(f(s)) = s` \n* **pick**: `\u003cS, Ks extends keyof S\u003e(...keys: Array\u003cKs\u003e) =\u003e Lens\u003cS, { [K in Ks]: S[K]; }\u003e`\n\n  Lens to a keys in a record\n\n  Note: the keys must always be present. \n* **key**: `\u003cS, K extends keyof S\u003e(k: K) =\u003e Lens\u003cS, S[K]\u003e`\n\n  Lens to a key in a record which may be missing\n\n  Note: setting the value to undefined removes the key from the record. \n* **def**: `\u003cA\u003e(missing: A) =\u003e Lens\u003cA, A\u003e`\n\n  Lens which refer to a default value instead of undefined\n\n  ```typescript\n  const store = Store.init({a: 1, b: 2} as Record\u003cstring, number\u003e)\n  const a_store = store.via(Lens.key('a')).via(Lens.def(0))\n  a_store.set(3)\n  store.get() // =\u003e {a: 3, b: 2}\n  a_store.get() // =\u003e 3\n  a_store.set(0)\n  store.get() // =\u003e {b: 2}\n  a_store.modify(x =\u003e x + 1)\n  store.get() // =\u003e {a: 1, b: 2}\n  ```\n* **seq**: `\u003cS, T, U\u003e(lens1: Lens\u003cS, T\u003e, lens2: Lens\u003cT, U\u003e) =\u003e Lens\u003cS, U\u003e`\n\n  Compose two lenses sequentially \n* **omit**: `\u003cS, K extends keyof S\u003e(...ks: Array\u003cK\u003e) =\u003e Lens\u003cS, Omit\u003cS, K\u003e\u003e`\n\n  Make a lens which omits some keys \n* **index**: `\u003cA\u003e(i: number) =\u003e Lens\u003cArray\u003cA\u003e, A\u003e`\n\n  Partial lens to a particular index in an array\n\n  ```typescript\n  const store = Store.init([0, 1, 2, 3])\n  const first = store.via(Lens.index(0))\n  first.get() // =\u003e 0\n  first.set(99)\n  store.get() // =\u003e [99, 1, 2, 3]\n  ```\n\n  Note: an exception is thrown if you look outside the array. \n### module Undo\n\nHistory zipper functions\n\n```typescript\nconst {undo, redo, advance, advance_to} = Undo\nconst store = Store.init(Undo.init({a: 1, b: 2}))\nconst modify = op =\u003e store.modify(op)\nconst now = store.at('now')\nnow.get() // =\u003e {a: 1, b: 2}\nmodify(advance_to({a: 3, b: 4}))\nnow.get() // =\u003e {a: 3, b: 4}\nmodify(undo)\nnow.get() // =\u003e {a: 1, b: 2}\nmodify(redo)\nnow.get() // =\u003e {a: 3, b: 4}\nmodify(advance)\nnow.update({a: 5})\nnow.get() // =\u003e {a: 5, b: 4}\nmodify(undo)\nnow.get() // =\u003e {a: 3, b: 4}\nmodify(undo)\nnow.get() // =\u003e {a: 1, b: 2}\nmodify(undo)\nnow.get() // =\u003e {a: 1, b: 2}\n```\n* **undo**: `\u003cS\u003e(h: Undo\u003cS\u003e) =\u003e Undo\u003cS\u003e`\n\n  Undo iff there is a past \n* **redo**: `\u003cS\u003e(h: Undo\u003cS\u003e) =\u003e Undo\u003cS\u003e`\n\n  Redo iff there is a future \n* **advance**: `\u003cS\u003e(h: Undo\u003cS\u003e) =\u003e Undo\u003cS\u003e`\n\n  Advances the history by copying the present state \n* **init**: `\u003cS\u003e(now: S) =\u003e Undo\u003cS\u003e`\n\n  Initialise the history \n* **advance_to**: `\u003cS\u003e(s: S) =\u003e (h: Undo\u003cS\u003e) =\u003e Undo\u003cS\u003e`\n\n  Advances the history to some new state \n* **can_undo**: `\u003cS\u003e(h: Undo\u003cS\u003e) =\u003e boolean`\n\n  Is there a state to undo to? \n* **can_redo**: `\u003cS\u003e(h: Undo\u003cS\u003e) =\u003e boolean`\n\n  Is there a state to redo to? \n### interface Undo\n\nHistory zipper \n* **now**: `S`\n\n  \n* **prev**: `Stack\u003cS\u003e`\n\n  \n* **next**: `Stack\u003cS\u003e`\n\n  \n### interface Stack\n\nA non-empty stack \n* **top**: `S`\n\n  \n* **pop**: `Stack\u003cS\u003e`\n\n  \n### module Requests\n\nUtility functions to make Elm/Redux-style requests\n\nA queue of requests are maintained in an array.\n\nTODO: Document and test. \n* **request_maker**: `\u003cR\u003e(store: Store\u003cArray\u003cR\u003e\u003e) =\u003e (request: R) =\u003e void`\n\n  Make a function for making requests \n* **request**: `\u003cR\u003e(store: Store\u003cArray\u003cR\u003e\u003e, request: R) =\u003e void`\n\n  Make a request \n* **process_requests**: `\u003cR\u003e(store: Store\u003cArray\u003cR\u003e\u003e, process: (request: R) =\u003e void) =\u003e () =\u003e void`\n\n  Process requests, one at a time\n\n  Retuns the off function. \n* **Diff**: `undefined`\n\n  \n* **Omit**: `undefined`\n\n  \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanr%2Freactive-lens","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdanr%2Freactive-lens","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanr%2Freactive-lens/lists"}