{"id":13452577,"url":"https://github.com/mweststrate/remmi","last_synced_at":"2025-07-31T17:09:27.140Z","repository":{"id":57353176,"uuid":"141304634","full_name":"mweststrate/remmi","owner":"mweststrate","description":"Nothing to see here. Go away","archived":false,"fork":false,"pushed_at":"2020-02-11T22:42:20.000Z","size":1275,"stargazers_count":87,"open_issues_count":3,"forks_count":2,"subscribers_count":15,"default_branch":"master","last_synced_at":"2025-07-01T13:07:32.760Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/mweststrate.png","metadata":{"files":{"readme":"readme.md","changelog":"changelog.md","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":"2018-07-17T14:54:42.000Z","updated_at":"2023-07-31T19:09:29.000Z","dependencies_parsed_at":"2022-09-19T01:01:40.575Z","dependency_job_id":null,"html_url":"https://github.com/mweststrate/remmi","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/mweststrate/remmi","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mweststrate%2Fremmi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mweststrate%2Fremmi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mweststrate%2Fremmi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mweststrate%2Fremmi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mweststrate","download_url":"https://codeload.github.com/mweststrate/remmi/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mweststrate%2Fremmi/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268075002,"owners_count":24191646,"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","status":"online","status_checked_at":"2025-07-31T02:00:08.723Z","response_time":66,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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-07-31T07:01:27.991Z","updated_at":"2025-07-31T17:09:27.113Z","avatar_url":"https://github.com/mweststrate.png","language":"TypeScript","funding_links":["https://www.buymeacoffee.com/mweststrate"],"categories":["TypeScript"],"sub_categories":[],"readme":"# Remmi\n\n_Materialized views for immutable data_\n\n\u003ca href=\"https://www.buymeacoffee.com/mweststrate\" target=\"_blank\"\u003e\u003cimg src=\"https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png\" alt=\"Buy Me A Coffee\" style=\"height: auto !important;width: auto !important;\" \u003e\u003c/a\u003eIf you think Remmi is an idea worth pursuing, encourage me with coffee :-). Or even better: discuss it with me over a real one the next opportunity!\n\n_This project is not actively maintained! So far, it has been a proof-of-concept. If you like the concept though, feel free to open an issue and consider becoming a maintainer, in which case this library might have a bright future! Without maintainers, it will no be actively developed any further._\n\n\n### Table of contents\n\n* [Introduction](#introduction)\n* [Features](#features)\n* [Core concepts](#core-concepts)\n* [API](#api)\n* [Recipes](#recipes)\n* [Detailed semantics](#detailed-semantics)\n* [Gotchas](#gotchas)\n* [Roadmap](#roadmap)\n* [FAQ](#faq)\n* [Credits](#credits)\n\n# Introduction\n\nRemmi is a library to create materialized views on top of immutable data.\nGranted, they are no materialized, but you conceptually Remmi works like a materialized view in the database on top of your immutable state tree:\n\n1. Derive data from an immutability based state tree\n2. Any future changes in the source state tree will automatically be reflected in the view\n3. Any writes made to the view will not update the view, but write-through and update the original state instead.\n\nWhere [immer](https://github.com/mweststrate/immer) solves the problem of \"how to update a deep, immutable state tree in a convenient way\",\n_remmi_ solves the opposite way: \"given a deep, immutable state tree, how to create reactive, bi-directional views that observe the immutable state?\".\nAs such, immer is basically cursors, mobx, immutable data and reactive streams smooshed together, helping you to transform your immutable data tree into something else, as reactively as possible.\n\nNote that \"view\" on the original state can be interpreted here in it's broadest term: derived data, UI (like React or lithtml), outgoing or incoming data streams or even OO-like data models!\n\n# Features\n\n* Single value, immutable state tree\n* Fully reactive\n* Transactional, atomic updates\n* Strongly typed\n* First class support for async processes\n* Mostly simple function composition\n* Extremely extensible, please share and publish your own transformers!\n* `this`-less\n* `null` safe (you can create, compose, chain cursors even when there is no backing value)\n\n\n# Core concepts\n\n## About cursors\n\nThe most important concept in remmi is the concept of cursors.\nLenses allow creating materialized views on the state, and enables reading from, writing to, and reacting to changes in the materialized view.\n\nTo support these features every lens exposes the following four core methods:\n\n1. `value()` returns the current, immutable value of the lens\n2. `update(thing)` applies an update to the current lens; that is, transforms and propagetes the update to wherever the lens got it's value from. Thing can be one of the following things:\n   * An [immer producer function](https://github.com/mweststrate/immer#api) where all changes that are made to the draft are applied to an immutable copy. This is the recommended way to update state\n   * An object. Merges the provided object with the current object using `Object.assign`\n   * A primitive value or array. Replaces the currenet state with the given value\n3. `subscribe(handler)`. The handler will called automatically every time the `value` of this lens is changed\n4. `do(transformations)`. Transforms the cursor into something else, more on that later!\n\n## Creating a store\n\nThe simplest way to get started with Remmi is to create a store using `createStore`.\n`createStore` create a very special cursor, one that actually holds state.\nBut basically, that is just an implementation detail, and you will interact with it like any other cursor.\n\n```javascript\nimport { createStore } from \"remmi\"\n\nconst profileCursor = createStore({\n      name: \"Michel\",\n      address: {\n            country: \"Amsterdam\"\n      }\n})\n\n// subscribe\nconst disposer = profileCursor.subscribe(profile =\u003e {\n      console.log(profile.address.country)\n})\n\n// update\nprofileCursor.update(draftProfile =\u003e {\n      draftProfile.address.country = \"The Netherlands\"\n})\n// prints: \"The Netherlands\"\n\ndisposer() // cancel the subscription\n\n// read the current value\nconsole.log(profileCursor.value().address.country)\n```\n\nThe post-fixing of the lens name with `Cursor` is a recommended best practice, as it makes it easy to distinguish cursors from the values they represent. For example it prevents variable shadowing in a case like: `profileCursor.subscribe(profile =\u003e {... })`.\n\n## Selecting data with cursors\n\nCursors are like materialized views in the database, they represent the latest state of the underlying data structure, and also accept updates to write data back. We can create new cursors by leveraging the `.do` method that all cursors expose, and passing in a `select` transformation, which grabs the `\"address\"` field from the profile and creates a cursor for that:\n\n```javascript\nimport { select } from \"remmi\"\n\nconst addressCursor = profileCursor.do(select(\"address\"))\n\naddressCursor.subscribe(address =\u003e {\n      console.log(\"New address is: \" + JSON.stringify(address))\n})\n\naddressCursor.update(address =\u003e {\n      address.city = \"Roosendaal\"\n})\n\n// prints { country: \"The Netherlands\", city: \"Roosendaal\"}\n\nprofileCursor.update(profile =\u003e {\n      profile.address.province = \"Noord Brabant\"\n})\n\n// prints { country: \"The Netherlands\", city: \"Roosendaal\", province: \"Noord Brabant\"}\n```\n\nCursors create a view on a part of the state, and are self contained units that can be both subscribe to, and write to the state that backs the tree.\nCursors are smart as they will only respond if the relevant part of the state has changed.\n\nCursors evaluate lazily, so they won't actually do any work until you start pulling values from them!\n\nIf you are using typescript, you will note that lenses are strongly typed. For example the following statement results in a compile errors:\n `profileCursor.do(\"hobbies\")` (profile doesn't have a `\"hobbies\"` field).\n\n_Tip: Because `select` is so common, there is a shortcut: `select` can be called directly as function on a cursor `profileCursor.select(\"address\")`_\n\n## Selector functions\n\nThe `select` transformation is not limited to just plucking fields from another cursor,\nthey can be used to derive all kinds of new views from a lens.\nFor that purpose `select` also accepts functions.\nThose functions should be pure and can construct arbitrarily new values from the tree (conceptually, this is very similar to reselect or computed values in MobX). For example:\n\n```javascript\nimport { createStore, select } from \"remmi\"\n\nconst todosCursor = createStore([\n      { title: \"Test Remmi\", done: true },\n      { title: \"Grok Remmi\", done: false}\n])\n\nconst tasksLeftCursor = todosCursor.do(select(\n      todos =\u003e todos.filter(todo =\u003e todo.done === false).length\n))\n\ntasksLeftCursor.subscribe(left =\u003e { console.log(\"Tasks left:\", left) })\n\ntodosCursor.update(todos =\u003e {\n      todos[0].done = false\n})\n// prints \"Tasks left: 2\"\n```\n\n## Transformers\n\nThe `.do` can be used to transform the cursors value into something else.\nMultiple transformers can be passed to `.do`, where the input of one is piped into the other, making it very similar to for example `Observable.pipe` in RxJS.\n\nBuilt in transformers are:\n* `all` - transforms a cursor that produces a collection (object or array) to a cursor of cursors, where each cursor forms the cursor of a field of the object\n* `connect` - connects a cursor to an external resources, and sets up an uni- or bi-directional connection to read new values from, and push new values to the external resource\n* `filter` - given a predicate filters over a collection. This is more efficient as as a `select` which uses `Array.filter`, as `mapReduce` is used under the hood, causing unmodified entries not to be re-processed\n* `fork` - creates a new cursor that has it's own state, which is initially the same as the old cursor's value. After forking, the forked cursor will keep track of all updates that are applied, and provides the possibility to play them back onto the original cursor\n* `fromStream` - Given an observable stream, reads all values from the stream and use it to update the cursor\n* `keys` - Produces all the keys of a collection, similar to `Object.keys`\n* `map` - maps over a collection, leveraging `mapReduce` under the hood to efficiently reuse mappings that weren't affected by an update\n* `readOnly` - transforms the current cursor into a read only cursor, which can be read from, but not written to\n* `render` - transforms the current cursor into a React component, that automatically keeps track of future updates to the cursor\n* `renderAll` - similar, but maps over a collection and produces a rendering per item in the collection\n* `select` - selects or produces a new value from the current state\n* `shallowEqual` - turns the cursor into a cursor that ignore updates that are shallowly equal to the previous value. Mostly useful after `select`\n* `subscribe` - subscribe a callback to listen to future cursor updates. `cursor.subscribe` can be used as shorthand\n* `tap` - tap into the stream, and prints a log message each time the cursor updates\n* `toStream` - creates an observable stream from the current cursor\n\n## Merging lenses\n\nThe `merge` function can combine multiple lenses into a new one. (It is quite comparable to `Promise.all`).\nThis is quite useful when you are working for example with 'foreign keys'.\n\n\n```javascript\nimport { createStore, select, merge } from \"remmi\"\n\nconst appCursor = createStore({\n      todos: [\n            { title: \"Test Remmi\", done: true, assignee: \"24\" },\n            { title: \"Grok Remmi\", done: false }\n      ],\n      users: {\n            \"24\": {\n                  name: \"Michel\"\n            }\n      }\n})\n\nconst firstTodoCursor = app.select(\"todos\").select(0)\nconst usersCursor = app.select(\"users\")\n\nconst assigneeNameCursor =\n      merge(usersCursor, firstTodoCursor),\n      select(([users, todo]) =\u003e\n            todo.assignee ? users[todo.assignee].name : undefined\n      )\n\nconsole.log(assigneeNameCursor.value())\n// prints: \"Michel\"\n```\n\nMerge produces a lens in itself, that just combines all the values of the input lenses as array.\n\nNote that this example is contrived, as the merge could also have been written using `select`.\nBut in big applications you might want to send only a part of your state around, and merge shows how to create a lens that combine individual pieces again.\n\nWhen combining multiple lenses or merges, Remmi will make sure that the lenses update glitch-free and in the right order.\n\n`merge` can merge lenses from multiple stores.\n\n# API\n\n_TODO: generate and link from JSDocs_\n\n# Recipes\n\n_TODO: work out  this section_\n\n- Advanced 5: Interoperability\n  - RxJS\n  - Redux\n- Read from websocket - https://codesandbox.io/s/o7y0mrvy86\n- Bidirectional sink between stores (unit test)\n- store from mouse handler - https://codesandbox.io/s/74252r73nq\n- Subscribe to reactive streams (unit tests)\n- simple example\n- references\n- testing a lens (model)\n- async process\n- connect to db\n* [ ] something cool with lithtml\n* [ ] Build something cool with https://codesandbox.io/s/m5lkpjm5mj\n* [ ] graphql\n* [ ] streams\n\n\n### connect to a redux store\n\n```javascript\nconst remmiStore = createStore(reduxStore.getState())\n\n// uni-directional sink (Redux -\u003e Remmi)\nconst cancel = remmiStore.do(\n      connect((_, sink) =\u003e reduxStore.subscribe(sink))\n)\n\n// bi-directional sink\nconst cancel = remmiStore.do(\n      connect((subcribe, sink) =\u003e {\n            // dispatch action if remmiStore was updated\n            subcribe(newState =\u003e {\n                  reduxStore.dispatch({\n                        type: \"REPLACE_THIS_AND_THAT\",\n                        payload: newState\n                  })\n            })\n\n            // sink Redux to Remmi\n            return reduxStore.subscribe(sink)\n      })\n)\nremmiStore.select(\"users\").subscribe(/*etc */)\n\ncancel() // stop syncing\n```\n\n# Detailed semantics\n\n## State versus Events\n\nImmer might look like a cross-over between reactive streams and lenses.\nWhich is correct.\nThe pipe and subscription mechanism are similar to reactive streams.\nThe differences however, is that conceptually Remmi cursors are designed to\ntransform _state_, while reactive streams reason over events and time.\n\nThe two have good compatability, but the choose for either of both should based be on the question whether you want to capture either:\n* The current state of the application, molding it in different values if needed\n* The events that happened over time, and reasoning about events to produce side effects\n\n## Cold and Hot cursors\n\nLike streams, a cursor can be either `hot` or `cold`.\nHot means that there is a subscription that directly or indirectly depends on the current value of the cursor.\nA cursor is `cold` if there is no such subscriptions.\nCold cursors are inefficient to read from, as they don't subscribe to their base cursors either (to prevent memory leaks).\nSo avoid reading `.value()` from a cursor that is cold!\n\n## Transactions\n\nCursor automatically apply a transaction per `.update()` call, subscribers are only updates ones the `.update` call finishes.\nIf there are multiple nested `.update` calls, subscribers will only be notified once the outer one finishes.\nA useful trick is to use `.update`, even without draft, to group multiple updates together, for example:\n\n```javascript\n\nstoreCursor.update(() =\u003e {\n      // without the wrapping update subscribers would be notified of a new state three times\n      const id1 = createBox(storeCursor, \"Roosendaal\", 100, 100)\n      const id2 = createBox(storeCursor, \"Prague\", 650, 300)\n      const id3 = createBox(storeCursor, \"Tel Aviv\", 150, 300)\n})\n\nexport function createBox(storeCursor, name, x, y) {\n    const id = randomUuid()\n    storeCursor.update(d =\u003e {\n        d.boxes[id] = { id, name, x, y }\n    })\n    return id\n}\n```\n\nAll subscribres are notified synchronosly as soon as a transaction ends, so, like in MobX update effects are immediately visible.\n\nUpdates are glitch free; that means that, when for example a `merge` is used to combine two lenses, and both lenses are updated, the `merge` will only run once, with both the updated values, and not for any intermediate state.\n\n## Testing lenses\n\nBecause lenses have a very uniform structure, testing them is issue, for example to test logic around the concept of addresses, in a unit test you could refrain from creating an entire user profile object, and just create a store for the address instead: `const addressCursor = createStore({ country: \"The Netherlands\", city: \"Roosendaal\", province: \"Noord Brabant\"})`. For the consumers of a cursor it doesn't matter whether a cursor is created using `createStore`, or using `select`, they will behave the same.\n\n# Gotchas\n\n_TODO: work out this section_\n\n* optimize: don't create selectors inline, but lift them\n* don't accidentally return, like: `lens.update(x =\u003e x.y += 2)`, use `void`, see immer\n* using `nothing` from immer\n\n# Roadmap\n\n_This project is not actively maintained! So far, it has been a proof-of-concept. If you like the concept though, feel free to open an issue and consider becoming a maintainer, in which case this library might have a bright future! Without maintainers, it will no be actively developed any further._\n\n* [ ] warn on cold reads\n* [ ] multiple args to select\n* [ ] write and generate documents\n* [ ] use hooks\n* [ ] fix todo example\n* [ ] by / groupBy (field) transformation\n* [ ] process `// TODO:` \u0026 `// optimization:` comments in the code base\n\n** Later **\n\n* [ ] different change propagation model that doesn't require 2 depth first walks?\n* [ ] join?\n* [ ] `.all()`, `.renderAll()` and `.mapReduce()` should detect splices (and not pass keys for arrays to handlers).\n* [ ] api to subscribe to patchespatch subscriptoin\n* [ ] separate export for react bindings\n* [ ] separate export for all views?\n* [ ] symbol supports (primitive, json etc)\n* [ ] nicer toStrings\n* [ ] generators: connect(generator), toGenerator: async* fn\n* [ ] fix optimization and todo comments\n\n# FAQ\n\nIs Remmi better than MobX? Well, that is not mine to decide :-). But my initial guess: No. And so far this is just an experimental package.\nIt is less efficient and syntactically more verbose. However if you prefer a single-immutable-value-source of truth, with less magic. You might fancy this one.\nEspecially since many more, more powerful transformations could be produced!\n\n\n# Credits\n\nRemmi stands on the shoulders of giants (which is a nice way of saying: Remmi just stole ideas left and right):\n* Materialized views in databases (see also: [turning the database inside out](https://www.youtube.com/watch?v=fU9hR3kiOK0))\n* Reactive streams like RxJS as immutable data distributing mechanism\n* Lense libraries (like baobab) to create a partially view on the state\n* MobX for reactive, sychnronous, atomic, glitchfree distribution of changes using a dependency tree\n* MobX-state-tree for providing models around immutable state\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmweststrate%2Fremmi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmweststrate%2Fremmi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmweststrate%2Fremmi/lists"}