{"id":17159264,"url":"https://github.com/akuzko/use-stash","last_synced_at":"2026-05-09T04:34:06.068Z","repository":{"id":57388409,"uuid":"183657578","full_name":"akuzko/use-stash","owner":"akuzko","description":"React hooks for app-wide data access and manipulation","archived":false,"fork":false,"pushed_at":"2021-08-26T21:21:36.000Z","size":70,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2026-04-08T09:20:03.708Z","etag":null,"topics":["action","actions","data","hook","hooks","react","store"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","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/akuzko.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-04-26T16:01:31.000Z","updated_at":"2022-10-20T09:56:54.000Z","dependencies_parsed_at":"2022-09-09T16:00:50.002Z","dependency_job_id":null,"html_url":"https://github.com/akuzko/use-stash","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/akuzko/use-stash","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akuzko%2Fuse-stash","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akuzko%2Fuse-stash/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akuzko%2Fuse-stash/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akuzko%2Fuse-stash/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/akuzko","download_url":"https://codeload.github.com/akuzko/use-stash/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akuzko%2Fuse-stash/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32807265,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-08T08:22:46.396Z","status":"online","status_checked_at":"2026-05-09T02:00:06.633Z","response_time":123,"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":["action","actions","data","hook","hooks","react","store"],"created_at":"2024-10-14T22:13:48.978Z","updated_at":"2026-05-09T04:34:06.053Z","avatar_url":"https://github.com/akuzko.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"React Stash Hooks\n=================\n\nReact hooks for app-wide data access and manipulation via actions. Minimalistic\nand easy-to-use solution inspired by [`redux`](https://redux.js.org/) family of projects.\n\n[![build status](https://img.shields.io/travis/akuzko/use-stash/master.svg?style=flat-square)](https://travis-ci.org/akuzko/use-stash)\n[![npm version](https://img.shields.io/npm/v/use-stash.svg?style=flat-square)](https://www.npmjs.com/package/use-stash)\n\n## Installation\n\n```\nnpm install --save use-stash\n```\n\n## Usage\n\n### 1. Define Namespace(s)\n\nBoth application data and actions to manipulate it are organized in namespaces.\nEach namespace is defined/initialized via `defStash` function:\n\n```js\nimport { defStash } from \"use-stash\";\n\nconst initialData = {\n  items: [],\n  details: {}\n};\n\ndefStash(\"todos\", initialData, ({defAction, reduce}) =\u003e {\n  defAction(\"getTodos\", () =\u003e {\n    fetch(\"/api/todos\")\n      .then(response =\u003e response.json())\n      .then(items =\u003e reduce(data =\u003e ({...data, items})));\n  });\n\n  defAction(\"getTodo\", (id) =\u003e {\n    fetch(`/api/todos/${id}`)\n      .then(response =\u003e response.json())\n      .then(details =\u003e reduce(data =\u003e ({...data, details})));\n  });\n});\n```\n\n### 2. Import Stash Namespace Definitions\n\nDon't forget to import all definitions on app initialization somewhere in your\nentry. Like so:\n\n```js\nimport \"stash/todos\";\nimport \"stash/projects\";\n// ...\n```\n\n### 3. Use Data and Actions\n\n`use-stash` provides following hooks for you to use\n\n#### `useData(path)`\n\nReturns a data object specified by `path`. The simplest value of `path` is\nnamespace name itself. But it can also represent deeply nested value\n(see **Granular Data Access** section bellow). Whenever this value gets\nupdated, your component will be updated as well.\n\nUsage example is provided in *useActions* section bellow.\n\n#### `useActions(namespace)`\n\nReturns object with all actions defined for specified `namespace`. This actions\ncan (and should) be used for all data manipulations.\n\nExample:\n\n```js\nimport { useEffect } from \"react\";\nimport { useData, useActions } from \"use-stash\";\n\nfunction TodosList() {\n  const {items} = useData(\"todos\");\n  const {getTodos} = useActions(\"todos\");\n\n  useEffect(() =\u003e {\n    getTodos();\n  }, []);\n\n  return (\n    // render list of todos\n  );\n}\n```\n\n#### `useStash(namespace)`\n\nReturns two-object array of namespace data and actions.\n\nExample:\n\n```js\nimport { useEffect } from \"react\";\nimport { useStash } from \"use-stash\";\n\nfunction TodoItem({id}) {\n  const [{details}, {getTodo}] = useStash(\"todos\");\n\n  useEffect(() =\u003e {\n    getTodo(id);\n  }, [id]);\n\n  return (\n    // render details for single item\n  );\n}\n```\n\n### Stash instance API\n\nWhen defining a new stash namespace, corresponding `Stash` instance is passed to\nsetup function, ready for you to destructure it for convenient usage. Each\ninstance of `Stash` is bound to specific namespace and has following\nproperties/methods:\n\n#### `namespace`\n\nReturns name of stash namespace. On definition, corresponds to first argument\nthat is passed to `defStash` function.\n\n#### `defAction(name, fn)`\n\nDefines an action identified by `name` under Stash's namespace that can then be\nused via `useActions` / `useStash` hooks. `fn` is the function that represents\na body of the action.\n\n#### `reduce([descriptor], fn)`\n\nUpdates data related to Stash namespace. Mandatory `fn` reducer function should\ntake a single argument - current Stash data and return a new data. There should\nbe no objects updated in-place. Optional `descriptor` parameter can provide\nauxiliary information on what reducer logic is about. It is not required, but\nmay be consumed by _mixins_, such as *logger* mixin (see bellow). Despite the\nfact it is optional, it comes as first argument due to better readability\nwhen given.\n\nExamples:\n\n```js\n// no descriptor\ndefAction(\"addItem\", (item) =\u003e {\n  reduce(data =\u003e [...data, item]);\n});\n\n// simple string descriptor\ndefAction(\"addItem\", (item) =\u003e {\n  reduce(\"addItem\", data =\u003e [...data, item]);\n});\n\n// array descriptor with additional data\ndefAction(\"addItem\", (item) =\u003e {\n  reduce([\"addItem\", item], data =\u003e [...data, item]);\n});\n```\n\n#### `get([path])`\n\nReturns namespace-scope data at specified `path`. Uses [`get-lookup`](https://www.npmjs.com/package/get-lookup)\nfor path resolution. If `path` is not provided, returns namespace data object\nitself.\n\n#### `callAction(actionName, ...actionArgs)`\n\nCalls an action `actionName`, passing `actionArgs` to the function that was\nused for this action definition via `defAction` function.\n\n#### `ns(namespace)`\n\nReturns a `Stash` instance for other namespace. This instance can be used for\nall sorts of things - gettings necessary data, calling actions, even reducing\n(changing) it's data, etc.\n\n#### `mixin(mixin)`\n\nAdds a mixin (see *mixins* section bellow), scoped to this stash namespace.\n\n#### `mixout(mixin)`\n\nRemoves mixin from list of mixins applied for this stash namespace. Can be used,\nfor instance, to opt-out mixin that was globally applied.\n\n### Common Use-Cases\n\n#### Granular Data Access\n\nUsually it may be much more efficient to use only small piece of data stored\nin namespace. This way your component will be re-rendered only if that small piece\ngets changed. To do so, you simply have to pass full path to the object you\nare interested in to `useData` hook call. `use-stash` uses\n[`get-lookup`](https://www.npmjs.com/package/get-lookup) package for fetching\ndeeply nested values. For example:\n\n```js\nfunction ItemStatus({id}) {\n  const status = useData(`todos.list.items.{id:${id}}.status`);\n\n  return \u003cdiv\u003e{ status.toUpperCase() }\u003c/div\u003e;\n}\n```\n\nThis component will be re-rendered only if status of item with corresponding\nvalue of `id` property in `todos.list.items` array changes.\n\nAlso, as can be seen from the example above, it is OK to use dynamic data paths,\ni.e. the ones that depend on changeable props or state values.\n\n#### Data Mapping on Access\n\nIn some scenarios, even when subscribed on a subset of data, there might be\nneed to extract some special value based on it, and re-render component only\nwhen this value changes. For instance, one may have a `TodoIndicator` component\nthat should render different value based on if there is *any incomplete todo*.\nThus, no matter if name of any todo changes, and if list itself changes,\nthat should not affect such component. To achieve this behavior, `useData` hook\naccepts a mapper function as it's second argument:\n```js\nfunction TodoIndicator() {\n  const allDone = useData(\"todos.list.items\", (items) =\u003e {\n    return items.every(item =\u003e item.status === \"completed\");\n  });\n\n  // ...\n}\n```\n\nFor even more complicated scenarios, where mapping function returns new\nidentities every time (such as arrays or objects), one may pass comparator\nfunction as third argument:\n```js\nimport isEqual from \"lodash/isEqual\";\n\nfunction TodoNames() {\n  const names = useData(\"todos.list.items\", (items) =\u003e {\n    return items.map(item =\u003e item.name);\n  }, isEqual);\n\n  // ...\n}\n```\n\n#### Accessing Data in Actions\n\nYou can use `get` method of `Stash` instance to access data of corresponding namespace:\n\n```js\ndefStash(\"todos\", initialState, ({defAction, reduce, get}) =\u003e {\n  defAction(\"removeTodo\", (index) =\u003e {\n    const id = get(`list.${index}.id`);\n\n    reduce((data) =\u003e {\n      return {...data, list: data.list.filter(item =\u003e item.id !== id)};\n    });\n  });\n});\n```\n\n#### Cross-namespace Interaction\n\nYou can use `ns` method to access `Stash` instance of another namespace. It can be\nused for fetching data from other namespaces, reducing it and even calling actions\ndefined in other namespaces:\n\n```js\ndefStash(\"todos\", initialState, ({defAction, reduce, get, ns}) =\u003e {\n  defAction(\"removeTodo\", (i) =\u003e {\n    const item = get(\"list\")[i];\n    const username = ns(\"session\").get(\"name\");\n\n    reduce((data) =\u003e {\n      const list = [...data.list];\n\n      list.splice(i, 1);\n      return {...data, list};\n    });\n\n    ns(\"logs\").reduce((data) =\u003e {\n      return [...data, `${username} removed item \"${item.title}\"`];\n    });\n    // if there is \"addEntry\" action defined in \"logs\" namespace, it can\n    // be called via\n    ns(\"logs\").callAction(\"addEntry\", `${username} removed item \"${item.title}\"`);\n  });\n});\n```\n\n### Mixins\n\nMixins are the way to add custom functionality to the one `use-stash` provides.\nIn essence, mixins are simple functions that take `stash` instance as first\nmandatory argument and return an object with overloaded stash props. Any\narguments passed to `mixin` function when applying mixin will be also passed to\nmixin function itself.\n\nMixins can be global, i.e. ones that apply to all defined namespaces and local,\napplied individually by each namespace.\n\nFor instance, if we want to have a mixin that makes stash data available for\ninspection at browser's console (via `window` object), it may look like this:\n\n```js\nwindow.appData = {};\n\nexport default function inspector(stash) {\n  const {namespace, init, reduce, get} = stash;\n\n  return {\n    init(data) {\n      window.appData[namespace] = data;\n      init(data);\n    },\n\n    reduce(...args) {\n      reduce(...args);\n      window.appData[namespace] = get();\n    }\n  };\n}\n```\n\nAnd then, to add this mixin globally, i.e. to apply it to every defined\nstash namespace, we need a single function call:\n\n```js\nimport { mixin } from \"use-stash\";\n\nmixin(inspector);\n```\n\nTo apply mixin only to specific stash namespace, we can use `Stash#mixin` function\navailable on stash namespace definition, i.e.:\n\n```js\ndefStash(\"inspectedStash\", initial, ({mixin}) =\u003e {\n  const {defAction, reduce} = mixin(inspector);\n\n  // rest of definitions\n});\n```\n\nIt is important to notice here that methods used for stash namespace definition\nare destructurized from the result of `mixin` function call, i.e. *after*\nmixin has been applied.\n\nIt is also possible to exclude mixin for single namespace if it has been globally\napplied before. For this you can use `Stash#mixout` function:\n\n```js\ndefStash(\"isolatedStash\", initial, ({mixout}) =\u003e {\n  const {defAction, reduce} = mixout(inspector);\n\n  // rest of definitions\n});\n```\n\nJust like with local `mixin` function, methods used for stash namespace definition\nshould be destructurized from `mixout` function call result.\n\n#### `mixins/logger`\n\nProvided out-of-the-box, `logger` mixin function adds logging support to stash\naction calls and data reducing in colorful and readable way. It also makes use\nof descriptor string/object that can be passed as first argument of `reduce` function.\n\nBy default, it colors logs for each namespace in it's own color (cycling through\npresets), but colors for each namespace can be set up manually via `colors`\nconfiguration option:\n\n```js\nimport { mixin } from \"use-stash\";\nimport { logger } from \"use-stash/mixins\";\n\nif (__DEV__) {\n  mixin(logger, {\n    colors: {\n      todos: \"blue\"\n    }\n  });\n}\n```\n\nAnd later on, for `todos` namespace we define, we can take advantage of having\nreducer's descriptor logged:\n\n```js\ndefStash(\"todos\", initialData, ({defAction, reduce}) =\u003e {\n  defAction(\"getTodos\", () =\u003e {\n    fetch(\"/api/todos\")\n      .then(response =\u003e response.json())\n      .then(items =\u003e reduce(\"getTodosSuccess\", data =\u003e ({...data, items})));\n  });\n\n  defAction(\"getTodo\", (id) =\u003e {\n    fetch(`/api/todos/${id}`)\n      .then(response =\u003e response.json())\n      .then((details) =\u003e {\n        reduce([\"getTodoSuccess\", details], data =\u003e ({...data, details}));\n        // ^ this is equivalent of:\n        //   reduce(\"getTodoSuccess\", data =\u003e ({...data, details}), [details]);\n      });\n  });\n});\n```\n\nIt is also possible to prevent certain actions and reductions to be logged,\nin case if they happen very frequently and you don't want them to spam console's\noutput. Such actions and reducers should be specified in the `except` mixin option:\n\n```js\nmixin(logger, {\n  except: [\"todos.toggleTodo\"]\n});\n```\n\n#### `mixins/actionReducer`\n\nGeneric stash reducer functions are not bound to actions they have originated\ndue to async nature of data update flow (they only keep track of the latest action\nbeing invoked). Since main purpose of `use-stash` is productivity and simplicity,\nfor easier development and logging it is possible to use per-action reducer functions.\nSuch reducers will generate descriptors corresponding to action they are bound to:\n\n```js\nimport { mixin } from \"use-stash\";\nimport { actionReducer } from \"use-stash/mixins\";\n\nmixin(actionReducer);\n````\nMake sure to add this mixin *after* adding `logger` mixin (see above). And then\nyou can have:\n\n```js\ndefStash(\"todos\", initialData, ({defAction}) =\u003e {\n  defAction(\"getTodos\", () =\u003e (reduce) =\u003e {\n    fetch(\"/api/todos\")\n      .then(response =\u003e response.json())\n      .then(items =\u003e reduce(data =\u003e ({...data, items})));\n  });\n\n  defAction(\"getTodo\", (id) =\u003e (reduce) =\u003e {\n    fetch(`/api/todos/${id}`)\n      .then(response =\u003e response.json())\n      .then((details) =\u003e {\n        reduce.success(data =\u003e ({...data, details}), [details]);\n        // ^ the last optional array argument specifies additional\n        //   data to be logged\n      });\n  });\n});\n```\nBellow you can see examples of what action reducer function correspond to:\n\n```js\ndefStash(\"todos\", ({defAction, reduce: stashReduce}) =\u003e {\n  defAction(\"getTodo\", (id) =\u003e (reduce) =\u003e {\n    // ...\n    reduce(() =\u003e data);                // stashReduce(\"getTodo\", () =\u003e data);\n    reduce(() =\u003e data, [data]);        // stashReduce([\"getTodo\", data], () =\u003e data);\n    reduce.success(() =\u003e data);        // stashReduce(\"getTodoSuccess\", () =\u003e data);\n    reduce.success(() =\u003e data, [data]) // stashReduce([\"getTodoSuccess\", data], () =\u003e data);\n    reduce.failure(() =\u003e ({}));        // stashReduce(\"getTodoFailure\", () =\u003e ({}));\n  });\n});\n````\n\n\n### HOCs for Class Components\n\nFor hook-less class-based React components you can use HOC-based approach. For this,\n`use-stash` provides 3 helper functions, similar to hooks: `withData`, `withActions`\nand `withStash`. The latter mixes functionality of the former two.\n\n#### `withData(dataMapping)`\n\nAllows to pass stash data to connected component's props. `dataMapping` is a plain\nobject, whose keys are prop names and values are paths to data that component is\ninterested in.\n\nExample:\n\n```js\nconst HocPage = withData({\n  username: \"session.name\",\n  todos: \"todos.list.items\",\n  logEntries: \"logs\"\n})(Page);\n```\n\nIn this example, underlying `Page` component will receive `username`, `todos`\nand `logEntries` props with values obtained from 3 different stash namespaces.\n\n#### `withActions(actionsMapping)`\n\nAllows to pass stash actions to connected component's props. `actionsMapping`\nis a plain object, whose keys are prop names and values are strings that specify\nstash namespace and action name, separated by dot.\n\nExample:\n\n```js\nconst HocLayout = withActions({\n  fetchSession: \"session.getSession\"\n})(Layout);\n```\n\nIn this example, underlying `Layout` component will receive `fetchSession` prop,\nwhich is a function corresponding to `getSession` action of `session` namespace.\n\n#### `withStash(dataMapping, actionsMapping)`\n\nAllows to pass both data and actions to connected component. A combination of\n`withData` and `withActions`.\n\nExample:\n\n```js\nconst HocPage = withStash({\n  username: \"session.name\",\n  todos: \"todos.list.items\",\n  logEntries: \"logs\"\n}, {\n  getItems: \"todos.getItems\",\n  addItem: \"todos.addItem\"\n})(Page);\n```\n\nKeep in mind that under the hood HOC wrappers are functional hook-based components,\ni.e. you still need React 16.8+ to use HOC helpers.\n\n### Hints and Tips\n\n#### Use `update-js` and `update-js/fp` packages\n\nTo keep action definitions thin, clean and simple, it is advised to use\n[`update-js`](https://www.npmjs.com/package/update-js) and/or\n[`update-js/fp`](https://www.npmjs.com/package/update-js#update-jsfp-module) packages\nwhich can drastically reduce complexity of your data reducers.\n\nFor example, imagine that we have following initial data structure:\n\n```js\nconst initialData = {\n  list: {\n    loading: false,\n    items: [],\n    pagination: {}\n  },\n  // ...\n};\n```\n\nEach item in the list identified by `id` property and has `checked` property\nthat can be changed via action.\n\nAnd we want to define couple of actions: one that loads a list, toggling it's\n`loading` flag, and other for toggling `checked` property of specific item in\nthe list.\n\nWith `update-js` it can look like this:\n\n```js\nimport { defStash } from \"use-stash\";\nimport update from \"update-js\";\n\ndefStash(\"todos\", initialData, ({defAction, reduce}) =\u003e {\n  defAction(\"getTodos\", () =\u003e {\n    reduce(data =\u003e update(data, \"list.loading\", true));\n\n    fetch(\"/api/todos\")\n      .then(response =\u003e response.json())\n      .then(items =\u003e {\n        reduce(data =\u003e update(data, {\n          \"list.loading\": false,\n          \"list.items\": items\n        }));\n      });\n  });\n\n  defAction(\"toggleChecked\", (id) =\u003e {\n    reduce(data =\u003e update.with(data, `list.items.{id:${id}}.checked`, checked =\u003e !checked));\n  });\n});\n```\n\nOr the same example with `update-js/fp` package looks even shorter:\n\n```js\nimport { defStash } from \"use-stash\";\nimport update from \"update-js/fp\";\n\ndefStash(\"todos\", initialData, ({defAction, reduce}) =\u003e {\n  defAction(\"getTodos\", () =\u003e {\n    reduce(update(\"list.loading\", true));\n\n    fetch(\"/api/todos\")\n      .then(response =\u003e response.json())\n      .then(items =\u003e {\n        reduce(update({\n          \"list.loading\": false,\n          \"list.items\": items\n        }));\n      });\n  });\n\n  defAction(\"toggleChecked\", (id) =\u003e {\n    reduce(update.with(`list.items.{id:${id}}.checked`, checked =\u003e !checked));\n  });\n});\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fakuzko%2Fuse-stash","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fakuzko%2Fuse-stash","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fakuzko%2Fuse-stash/lists"}