{"id":13394697,"url":"https://github.com/ivan-kleshnin/reactive-states","last_synced_at":"2025-04-15T02:37:11.347Z","repository":{"id":92372814,"uuid":"54320723","full_name":"ivan-kleshnin/reactive-states","owner":"ivan-kleshnin","description":"Reactive state implementations (brainstorming)","archived":false,"fork":false,"pushed_at":"2018-03-01T07:58:25.000Z","size":18,"stargazers_count":51,"open_issues_count":0,"forks_count":1,"subscribers_count":7,"default_branch":"master","last_synced_at":"2024-02-17T14:33:31.447Z","etag":null,"topics":["comparison","frp","reactive","reactivity","redux","state-management"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ivan-kleshnin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-03-20T14:04:42.000Z","updated_at":"2024-05-30T08:04:40.531Z","dependencies_parsed_at":"2023-05-17T01:00:19.863Z","dependency_job_id":null,"html_url":"https://github.com/ivan-kleshnin/reactive-states","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ivan-kleshnin%2Freactive-states","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ivan-kleshnin%2Freactive-states/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ivan-kleshnin%2Freactive-states/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ivan-kleshnin%2Freactive-states/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ivan-kleshnin","download_url":"https://codeload.github.com/ivan-kleshnin/reactive-states/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248995103,"owners_count":21195497,"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":["comparison","frp","reactive","reactivity","redux","state-management"],"created_at":"2024-07-30T17:01:28.574Z","updated_at":"2025-04-15T02:37:11.032Z","avatar_url":"https://github.com/ivan-kleshnin.png","language":"JavaScript","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"readme":"# Reactive states\n\nRxJS is used for code samples but everything is supposed to be technology agnostic.\n\n---\n\nReactive state is a state implemented by reducing values over time. There are two main questions to be asked for every particular library which provides a \"state solution\". Namely: the exact implementation of reducers and the number of them (one vs few vs many).\n\nThis two questions are completely orthogonal yet framework authors does not emphasize this fact enough.\nSaying \"Redux architecture\" the first person may have a \"single store\" concept in mind. The second one may imply \"action-style reducers\" while the third may assume a strict combination of both.\n\nLet's start with possible reducer implementations.\n\n## Reducer implementations\n\n### Data reducer\n\n```js\nlet {Subject} = require(\"rx\")\n\nlet update = new Subject() // update channel\n\nlet state = update \n  .startWith(0)          // initial value\n  .scan((s, x) =\u003e s + x) // data reducer\n\nstate.subscribe(s =\u003e console.log(\"state:\", s))\n\n// TEST\nupdate.onNext(1)  // +1\nupdate.onNext(2)  // +2\nupdate.onNext(3)  // +3\nupdate.onNext(-3) // -3\nupdate.onNext(-2) // -2\nupdate.onNext(-1) // -1\n```\n\n```\nstate: 0\nstate: 1\nstate: 3\nstate: 6\nstate: 3\nstate: 1\nstate: 0\n```\n\nNow what if you want to reset state to some initial (seed) value? Obviously, you can't `update.onNext(0)` because it means `s + 0`. The best thing you can do is `update.onNext((+/-)currentState)` and you can guess how quick this becomes unreasonable.\n\n#### Benefits\n\n* the simplest one\n\n#### Drawbacks\n\n* fails to represent all but the simplest action sets\n* fails to represent nested state\n\n#### Conclusion\n\n* is perfect for cases it satisfies ;)\n\n### Action Reducer \n\n#### Redux way\n\n```js\nlet {Subject} = require(\"rx\")\n\nlet update = new Subject() // update channel\n\nlet ADD = \"+\"\nlet SUBTRACT = \"-\"\n\nlet state = update \n  .startWith(0) \n  .scan((state, action) =\u003e {\n    switch (action.type) {\n      case ADD:\n        return state + action.value\n      case SUBTRACT:\n        return state - action.value\n      default:\n        throw Error(\"unsupported action\")\n    }\n  })\n\nstate.subscribe(s =\u003e console.log(\"state:\", s))\n\n// TEST\nupdate.onNext({type: \"+\", value: 1}) // +1\nupdate.onNext({type: \"+\", value: 2}) // +2\nupdate.onNext({type: \"-\", value: 2}) // -2\nupdate.onNext({type: \"-\", value: 1}) // -1\n```\n\n```\nstate: 0\nstate: 1\nstate: 3\nstate: 1\nstate: 0\n```\n\n#### Alternative way\n\n```js\nlet {Subject} = require(\"rx\")\n\nlet update = new Subject() // update channel\n\nlet state = update \n  .startWith(0)\n  .scan((state, action) =\u003e {\n    switch (action[0]) {\n      case \"+\":\n        return state + action[1]\n      case \"-\":\n        return state - action[1]\n      default:\n        throw Error(\"unsupported action\")\n    }\n  })\n\nstate.subscribe(s =\u003e console.log(\"state:\", s))\n\n// TEST\nupdate.onNext([\"+\", 1]) // +1\nupdate.onNext([\"+\", 2]) // +2\nupdate.onNext([\"-\", 2]) // -2\nupdate.onNext([\"-\", 1]) // -1\n```\n\n```\nstate: 0\nstate: 1\nstate: 3\nstate: 1\nstate: 0\n```\n\nNow what if we want to reset counter here? It's just a matter of adding a new switch branch.\n\n#### Benefits\n\n* can describe arbitrarily complex action sets\n* reducer contains a list of possible actions\n* can represent nested state\n* easy to log actions\n\nThe most interesting benefit of this one is the ability to log actions easily.\nYou just need to prepend a reducer with a logger as all you need is there (in the pipe).\nThis comes in handy for scrapers where you need to log all pages you download, all documents you save, etc.\n\n#### Drawbacks\n\n* can't extend reducer you don't control (need to create new one and compose them: expression problem)\n* reducer contains a list of possible actions (fast growing if / switch statement: expression problem)\n* incidental complexity (middlemen) (data structure kinda conveys a list of possible actions already)\n* need to edit multiple files to implement one action (action \u0026 reducer files) \n\n#### Conclusion\n\n* are good for basic-to-medium action sets (?)\n* or if you need a no-brainer for logging\n\n### Functional Reducer \n\n```js\nlet {add, curry, flip, subtract} = require(\"ramda\")\nlet {Subject} = require(\"rx\")\n\n// scanFn :: s -\u003e (s -\u003e s) -\u003e s\nlet scanFn = curry((state, updateFn) =\u003e {\n  if (typeof updateFn != \"function\" || updateFn.length != 1) {\n    throw Error(\"updateFn must be a function with arity 1, got \" + updateFn)\n  } else {\n    return updateFn(state)\n  }\n})\n\nlet update = new Subject() // update channel\n\nlet state = update \n  .startWith(0)\n  .scan(scanFn)\n\nstate.subscribe(s =\u003e console.log(\"state:\", s))\n\n// TEST\nupdate.onNext(add(1))            // +1\nupdate.onNext(add(2))            // +2\nupdate.onNext(add(3))            // +3\nupdate.onNext(flip(subtract)(3)) // -3\nupdate.onNext(flip(subtract)(2)) // -2\nupdate.onNext(flip(subtract)(1)) // -1\n```\n\n#### Benefits\n\n* open action set (not limited by hardcoded actions)\n* composable actions (`update3 = compose(update2, update1)`)\n\n#### Drawbacks\n\n* open action set (can be viewed as drawback)\n* hard to log actions (stream of functions is obscure)\n\n#### Conclusion\n\n* pairs nicely with currying\n* pairs nicely with lensing\n* is appropriate for most action sets out there (?)\n\n## Derived states\n\nWhat is **derived state**? It's a state which is produced from other state (common or derived as well).\u003cbr/\u003e\n\"So why is this not just a state? Or just a stream?\" – you can ask. \"Why we need additional terms besides MVC, MVI and whatnot?\"\nUnfortunately, world is too complex for acronyms.\n\nForm errors is an example of **derived state**. It's a state because it's renderable.\nYou render input errors in the same way as input values. But there are several reasons to treat them differently.\n\n1. Minimal state size is desirable for serialization (transfer, etc.)\n2. Keep one source of truth (avoid unsync cases like `y == f(x) /* by formula */ y != f(x) /* by fact */`.\n3. Preserve reactivity (derivables are either passive or reactive).\n\nThis should detract you from the idea of mixing common and derived states in a single reducer.\nWhat options are left? \n\n1) Derived state is a stream (not usual but stateful one). \n\n```js\nlet derived = state.map((s) =\u003e {\n  let d = ...\n  // calculate every dependency\n  return d\n})\n```\n2) Derived state is a record of streams.\n\n```js\nlet derived = {\n  d1: state.map((s) =\u003e ...),\n  d2: state.map((s) =\u003e ...),\n}\n```\n\nIn a single-stream version, `state.counter` increasing every second will trigger a recalculation of derived state every second.\nIn a multi-stream version you can describe reactive dependencies per-field `someFlag.debounce(10)` but passing stuff between functions is encomplicated. You can also mix-n-match approaches:\n\n```js\nlet derived = {          // RECORDS OF STREAMS\n  foo: fooStream,        // single-value stream\n  bar: barStream,        // ...\n  flags: state.map(...), // multi-value stream\n}\n```\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fivan-kleshnin%2Freactive-states","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fivan-kleshnin%2Freactive-states","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fivan-kleshnin%2Freactive-states/lists"}