{"id":18245013,"url":"https://github.com/edgeapp/redux-keto","last_synced_at":"2025-04-04T13:31:59.583Z","repository":{"id":42201594,"uuid":"102723153","full_name":"EdgeApp/redux-keto","owner":"EdgeApp","description":"A tool for building \"fat\" reducers","archived":false,"fork":false,"pushed_at":"2023-10-10T22:28:56.000Z","size":594,"stargazers_count":6,"open_issues_count":8,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-14T11:02:28.078Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","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/EdgeApp.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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":"2017-09-07T10:18:15.000Z","updated_at":"2022-11-18T17:59:21.000Z","dependencies_parsed_at":"2023-10-11T00:59:37.072Z","dependency_job_id":null,"html_url":"https://github.com/EdgeApp/redux-keto","commit_stats":{"total_commits":50,"total_committers":2,"mean_commits":25.0,"dds":"0.040000000000000036","last_synced_commit":"d4f991388f79fa357f5090b01c961bee6bfad6ff"},"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EdgeApp%2Fredux-keto","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EdgeApp%2Fredux-keto/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EdgeApp%2Fredux-keto/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EdgeApp%2Fredux-keto/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/EdgeApp","download_url":"https://codeload.github.com/EdgeApp/redux-keto/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247185116,"owners_count":20897905,"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-05T09:18:35.866Z","updated_at":"2025-04-04T13:31:59.286Z","avatar_url":"https://github.com/EdgeApp.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# redux-keto\n\n\u003e A tool for building fat reducers\n\n[![npm downloads](https://img.shields.io/npm/dm/redux-keto.svg?style=flat-square)](https://www.npmjs.com/package/redux-keto)\n\nThe Redux architecture works best when the reducers contain as much business logic as possible. Doing this in practice is hard, though, since reducers can't pass values between each other.\n\nThis library provides a way to build \"fat reducers\", which take an extra `next` argument in addition to the normal `state` and `action` arguments. Fat reducers use this extra parameter to pass values between each other in a fully-reactive, auto-updating way.\n\nFat reducers work seamlessly with normal reducers, so there are no big changes to the way Redux works. Just use fat reducers wherever they make sense.\n\n## Table of contents\n1. [Example](#example)\n2. [Derived state](#derived-state)\n3. [Customizing `next`](#customizing-next)\n   1. [Reducer lists](#reducer-lists)\n   1. [Isolated reducers](#isolated-reducers)\n4. [Implementation details](#implementation-details)\n\n## Example\n\nSuppose an app has two pieces of state, a `counter` and a user-settable `maxCount`. The counter should never exceed `maxCount`, so it also needs update itself whenever `maxCount` changes.\n\nUsing `redux-keto`, the `maxCount` reducer can be perfectly normal:\n\n```js\nfunction maxCount (state = 0, action) {\n  return action.type === 'CHANGE_MAX_COUNT' ? action.payload : state\n}\n```\n\nThe `counter` reducer is only a little more complicated, since it needs to consider the `maxCount` state:\n\n```js\nfunction counter (state = 0, action, next) {\n  return Math.min(\n    next.maxCount,\n    action.type === 'INCREMENT' ? state + action.payload : state\n  )\n}\n```\n\nFinally, the `buildReducer` function from `redux-keto` combines these two reducers into one:\n\n```js\nimport { buildReducer } from 'redux-keto'\n\nexport const rootReducer = buildReducer({ maxCount, counter })\n```\n\nThat's it! Compared to the [alternatives](https://github.com/Airbitz/redux-keto/blob/master/docs/bad-alternatives.md), this is incredibly simple.\n\nEverything is fully reactive, since the `next` parameter reflects the upcoming state of each reducer. In other words, `next.maxCount` contains the result of running the `maxCount` reducer on the current action. This means that dispatching a `CHANGE_MAX_COUNT` action will automatically update the counter without any extra work. The `counter` reducer always keeps itself up to date by checking the new `maxCount` state on each action.\n\n## Derived state\n\nTo derive a value from some existing state, just create a fat reducer that ignores its `state` and `action` parameters:\n\n```js\nfunction countIsEven (state, action, next) {\n  return next.counter % 2 === 0\n}\n```\n\nNow `countIsEven` will stay in sync with the `counter` no matter what happens. Because this is just a normal reducer, it will also appear in `next` so other reducers can access it.\n\nTo optimize cases where the state hasn't changed, fat reducers also receive a `prev` parameter, which holds the state *before* the current action. The reducer can compare the two states to see if it needs to do any work:\n\n```js\nfunction countIsOdd (state, action, next, prev) {\n  if (next.counter === prev.counter) return state\n\n  return next.counter % 2 === 1\n}\n```\n\nNow the `countIsOdd` calculation will only run when the counter actually changes.\n\nTo automate this, `redux-keto` provides a `memoizeReducer` function. This function works a lot like the [reselect](https://github.com/reactjs/reselect) library, but for reducers:\n\n```js\nconst isOdd = memoizeReducer(\n  next =\u003e next.counter,\n  counter =\u003e counter % 2 === 1\n)\n```\n\nThe last parameter to `memoizeReducer` is the actual calculation. All the previous parameters are functions that grab items out of the next state. If all the items are the equal (`===`) to their previous values, `memoizeReducer` just returns the previous state. Otherwise, `memoizeReducer` runs the calculation with the items as parameters.\n\n## Customizing `next`\n\nBy default, `buildReducer` passes `next` through to its children unchanged. If `buildReducer` doesn't receive a `next` parameter, it initializes `next` with its own children. This is why the initial example works—the top-level `buildReducer` doesn't receive a `next` parameter, so it sets up a `next` object with the future `maxCount` and `counter` states as properties. This also means that if `buildReducer` happens to be the top-most reducer in the Redux store, `next` will match the Redux state tree returned by `getState()`.\n\nTo customize this behavior, just pass a `makeNext` function as the second parameter to `buildReducer`:\n\n```js\ncounterState = buildReducer(\n  buildReducer({ maxCount, counter }),\n  (next, children, id) =\u003e children\n}\n```\n\nThe `makeNext` function accepts three parameters, `next`, `children`, and `id`. The `next` parameter is just whatever was passed to `buildReducer`, the `children` parameter holds the future `buildReducer` state, and `id` is the current reducer's name.\n\nIn this example, `makeNext` just returns the `children` unconditionally. This means that the `counter` reducer can always refer to `next.maxCount`, no matter where it is located in the Redux state tree.\n\n### Reducer lists\n\nApplications often manage lists of things. For example, a chat platform might manage multiple conversations, each with its own state. To handle cases like this, `redux-keto` provides a `mapReducer` function:\n\n```js\nimport { mapReducer } from 'redux-keto'\n\nconst chatsById = mapReducer(\n  chatReducer,\n\n  // The list of ids:\n  next =\u003e next.activeChatIds,\n\n  // Set up `next` for each child:\n  (next, children, id) =\u003e ({\n    chatId: id,\n    root: next,\n    get self () {\n      return children[id]\n    }\n  })\n)\n```\n\nThe first `mapReducer` parameter is the reducer to replicate, and the second parameter returns a list of ids. There will be one `chatReducer` for each unique id (duplicates are ignored).\n\nThe final parameter is a `makeNext` function, just like the one `buildReducer` accepts. If this isn't provided, `mapReducer` will create a default `next` parameter with the following properties:\n\n* `id` - the current child's id.\n* `root` - the `next` parameter passed to `mapReducer`, or `children` if `next` is undefined.\n* `self` - the current child's future state.\n\nIf you want to replicate this behavior yourself, be sure to implement `self` using a getter function, as shown above. Otherwise, you may get a [circular reference error](#circular-dependencies).\n\n### Isolated reducers\n\nTo customize both the actions and `next` parameter going into an individual reducer, use `filterReducer`. This is especially useful with `mapReducer`, since it allows each child reducer to act like its own stand-alone sub-store:\n\n```js\nconst chatReducer = filterReducer(\n  chatSubsystem,\n\n  // Filter the actions (receives the outer `next` parameter):\n  (action, next) =\u003e {\n    if (action.payload.chatId === next.chatId) {\n      return action\n    }\n    if (action.type === 'LOGIN') {\n      return { type: 'CHAT_INIT'}\n    }\n  },\n\n  // Adjust `next` for the child:\n  next =\u003e ({ settings: next.root.chatSettings })\n)\n```\n\nIn this example, the `chatReducer` will only receive actions where the `chatId` matches its own `chatId`. It will also receive a `CHAT_INIT` message when the outer system receives a `LOGIN` action.\n\n## Implementation details\n\n### Circular dependencies\n\nThe `buildReducer` and `mapReducer` functions create the illusion of time travel by passing their own future state into their children. They achieve this magic using memoized lazy evaluation. Each property of the `next` object is actually a getter that calls the corresponding child reducer to calculate the new state on the spot (unless the reducer has already run).\n\nThis means that circular dependencies will not work. If a reducer tries to read its own output, even indirectly, it will fail with a `ReferenceError`.\n\n### Default state\n\nThe first time a reducer runs, it has no previous state. On the other hand, allowing `prev` to just be `undefined` would make writing reducers much more difficult. To solve this, `redux-keto` looks for a property called `defaultState` on each reducer function. If it finds one, it uses that as the initial state and builds the `prev` parameter based on that. This allows `prev` to have a useful tree structure, even on the first run.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedgeapp%2Fredux-keto","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fedgeapp%2Fredux-keto","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedgeapp%2Fredux-keto/lists"}