{"id":13475936,"url":"https://github.com/PabloSzx/react-state-selector","last_synced_at":"2025-03-27T01:30:36.519Z","repository":{"id":54515086,"uuid":"241411712","full_name":"PabloSzx/react-state-selector","owner":"PabloSzx","description":"Performant, type safe and easy React global state manager","archived":false,"fork":false,"pushed_at":"2023-10-16T23:39:56.000Z","size":9913,"stargazers_count":39,"open_issues_count":6,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-23T22:39:35.003Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://pabloszx.github.io/react-state-selector/","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/PabloSzx.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,"governance":null,"roadmap":null,"authors":null}},"created_at":"2020-02-18T16:33:16.000Z","updated_at":"2024-11-04T16:13:06.000Z","dependencies_parsed_at":"2024-01-16T07:23:25.574Z","dependency_job_id":"931f9622-3e0b-414b-931e-6791f5c2979e","html_url":"https://github.com/PabloSzx/react-state-selector","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PabloSzx%2Freact-state-selector","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PabloSzx%2Freact-state-selector/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PabloSzx%2Freact-state-selector/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PabloSzx%2Freact-state-selector/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/PabloSzx","download_url":"https://codeload.github.com/PabloSzx/react-state-selector/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245764594,"owners_count":20668447,"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-07-31T16:01:24.882Z","updated_at":"2025-03-27T01:30:35.391Z","avatar_url":"https://github.com/PabloSzx.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# React State Selector\n\n![logo](https://i.imgur.com/dIeukBc.png)\n\n[![codecov](https://codecov.io/gh/PabloSzx/react-state-selector/branch/master/graph/badge.svg?token=86B359Hwdw)](https://codecov.io/gh/PabloSzx/react-state-selector)\n[![npm version](https://badge.fury.io/js/react-state-selector.svg)](https://badge.fury.io/js/react-state-selector)\n[![bundlephobia](https://badgen.net/bundlephobia/minzip/react-state-selector)](https://bundlephobia.com/result?p=react-state-selector)\n[![license](https://badgen.net/github/license/pabloszx/react-state-selector)](https://github.com/pabloszx/react-state-selector)\n[![combined statuses](https://badgen.net/github/status//pabloszx/react-state-selector)](https://github.com/pabloszx/react-state-selector)\n\n## **React global state management**, the performant, type safe and easy way\n\n```sh\npnpm add react-state-selector\n# or\nnpm install react-state-selector\n# or\nyarn add react-state-selector\n```\n\n---\n\n## What if we combine [**immer**](https://github.com/immerjs/immer), [**reselect**](https://github.com/reduxjs/reselect) and even [**React Context API**](https://reactjs.org/docs/context.html) to make a performant and expressive React global state manager?\n\n\u003e Check **https://pabloszx.github.io/react-state-selector** for more detailed examples and use cases.\n\n\u003c!-- START doctoc generated TOC please keep comment here to allow auto update --\u003e\n\u003c!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --\u003e\n**Table of Contents**\n\n- [Features](#features)\n- [Basic Usage](#basic-usage)\n- [Basic Context Usage](#basic-context-usage)\n- [Default API](#default-api)\n  - [createStore](#createstore)\n    - [produce: _function(draft =\u003e void | TStore): TStore_](#produce-_functiondraft--void--tstore-tstore_)\n    - [asyncProduce: _function(async draft =\u003e void | TStore): Promise TStore_](#asyncproduce-_functionasync-draft--void--tstore-promise-tstore_)\n    - [useStore: _function(): TStore_](#usestore-_function-tstore_)\n  - [createStoreContext](#createstorecontext)\n    - [useStore: _function(): TStore_](#usestore-_function-tstore_-1)\n    - [useProduce: _function(): { produce, asyncProduce }_](#useproduce-_function--produce-asyncproduce-_)\n- [Custom API](#custom-api)\n  - [Custom Hooks](#custom-hooks)\n  - [Custom Actions](#custom-actions)\n    - [Actions](#actions)\n    - [Async Actions](#async-actions)\n- [**localStorage** data persistence and **DevTools**](#localstorage-data-persistence-and-devtools)\n- [**Map, Set, old browsers, React Native** support](#map-set-old-browsers-react-native-support)\n\n\u003c!-- END doctoc generated TOC please keep comment here to allow auto update --\u003e\n\n## Features\n\n- [x] Redux **DevTools** support\n- [x] **async** actions (_**redux-thunk** alike_)\n- [x] **TypeScript** first class support\n- [x] **_reselect_** createSelector support\n- [x] Easy and efficient **localStorage** data persistence\n- [x] Support for **AsyncStorage** data persistence (_for example, for [React Native AsyncStorage](https://reactnative.dev/docs/0.53/asyncstorage)_), just add it inside the `persistenceMethod` option in `createStore` or `createStoreContext`, and use the helpers `isReady` _(createStore)_ or `useIsReady` _(createStoreContext)_ to wait for them to be ready before it's usage.\n\n## Basic Usage\n\nFor simple global stores you can use createStore.\n\n```tsx\nimport { createStore } from \"react-state-selector\";\n\nconst {\n  hooks: { useCountA, useCountB },\n  actions: { incrementA, incrementB },\n} = createStore(\n  {\n    countA: 0,\n    countB: 0,\n  },\n  {\n    hooks: {\n      useCountA: ({ countA }) =\u003e {\n        // Here reselect will automatically memoize this selector\n        // and only rerender the component when absolutely needed\n        return countA;\n      },\n      useCountB: ({ countB }) =\u003e {\n        return countB;\n      },\n    },\n    actions: {\n      incrementA: (n: number) =\u003e (draft) =\u003e {\n        // Here you can mutate \"draft\", and immer will\n        // automatically make the immutable equivalent\n        draft.countA += n;\n      },\n      incrementB: (n: number) =\u003e (draft) =\u003e {\n        draft.countB += n;\n      },\n    },\n  }\n);\n\n// ...\n\nconst CounterA = () =\u003e {\n  const a = useCountA();\n\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eCounter A\u003c/h1\u003e\n      \u003cspan\u003e{a}\u003c/span\u003e\n      \u003cbutton onClick={() =\u003e incrementA(1)}\u003e+\u003c/button\u003e\n    \u003c/div\u003e\n  );\n};\n\nconst CounterB = () =\u003e {\n  const b = useCountB();\n\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eCounter B\u003c/h1\u003e\n      \u003cspan\u003e{b}\u003c/span\u003e\n      \u003cbutton onClick={() =\u003e incrementB(2)}\u003e+\u003c/button\u003e\n    \u003c/div\u003e\n  );\n};\n```\n\nThis library uses type inference to automatically help you with auto-completion and type-safety, **even if you only use JavaScript and not TypeScript!**.\n\n## Basic Context Usage\n\nIf you need multiple instances of a specific store you can use the React Context API to make specific instances of the store.\n\n```tsx\nimport { createStoreContext } from \"react-state-selector\";\n\nconst {\n  hooks: { useCountA, useCountB },\n  useActions,\n  Provider,\n} = createStoreContext(\n  {\n    countA: 0,\n    countB: 0,\n  },\n  {\n    hooks: {\n      useCountA: ({ countA }) =\u003e {\n        // Here reselect will automatically memoize this selector\n        // and only rerender the component when absolutely needed\n        return countA;\n      },\n      useCountB: ({ countB }) =\u003e {\n        return countB;\n      },\n    },\n    actions: {\n      incrementA: (n: number) =\u003e (draft) =\u003e {\n        // Here you can mutate \"draft\", and immer will\n        // automatically make the immutable equivalent\n        draft.countA += n;\n      },\n      incrementB: (n: number) =\u003e (draft) =\u003e {\n        draft.countB += n;\n      },\n    },\n  }\n);\n\n// ...\n\nconst CounterA = () =\u003e {\n  const a = useCountA();\n  const { incrementA } = useActions();\n\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eCounter A\u003c/h1\u003e\n      \u003cspan\u003e{a}\u003c/span\u003e\n      \u003cbutton onClick={() =\u003e incrementA(1)}\u003e+\u003c/button\u003e\n    \u003c/div\u003e\n  );\n};\n\nconst CounterB = () =\u003e {\n  const b = useCountB();\n  const { incrementB } = useActions();\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eCounter B\u003c/h1\u003e\n      \u003cspan\u003e{b}\u003c/span\u003e\n      \u003cbutton onClick={() =\u003e incrementB(2)}\u003e+\u003c/button\u003e\n    \u003c/div\u003e\n  );\n};\n\n// ...\n\nconst Counters = () =\u003e {\n  return (\n    \u003c\u003e\n      \u003cProvider\u003e\n        \u003cCounterA /\u003e\n        \u003cCounterB /\u003e\n      \u003c/Provider\u003e\n      \u003cProvider\u003e\n        \u003cCounterB /\u003e\n      \u003c/Provider\u003e\n    \u003c/\u003e\n  );\n};\n```\n\n## Default API\n\nBy default every created store gives a couple of out of the box functionality, if you don't use them it's okay, but they could end up being handy:\n\n### createStore\n\n#### produce: _function(draft =\u003e void | TStore): TStore_\n\n- Synchronous change to the store state, you should give it a function that will mutate the state and it will give the resulting global state after the transformation. Don't worry about mutating the draft, immer will do the transformations. At first it might feel weird if you are used to making the immutable equivalent of every mutation and using ~~_(abusing)_~~ the spread syntax.\n- If you **return** something in the draft function, it will transform the entire global state into that value. [_Read more_](https://immerjs.github.io/immer/docs/return).\n- If you **don't** give it a function, it will work simply as a **state getter**, so you can check the global state anytime without any restriction.\n\n```ts\nconst state = produce((draft) =\u003e {\n  draft.a += 1;\n});\nconsole.log(produce() === state); // true\n```\n\n#### asyncProduce: _function(async draft =\u003e void | TStore): Promise TStore_\n\n- Asynchronous change to the store state, you should give it an async function that will mutate the state and it will give a promise of the resulting global state after the transformation.\n- It is often better to use [custom actions](#custom-actions) for dealing with asynchronous requests, since here when you start the async function, you might had received a stale draft state after the request is done.\n- You shouldn't rely on this feature to transform the entire state as with [produce](#produce-functiondraft--void--tstore-tstore) or [custom actions](#custom-actions);\n\n```ts\nconst state = await asyncProduce(async (draft) =\u003e {\n  draft.users = await (await fetch(\"/api/users\")).json();\n});\nconsole.log(produce() === state); // true\n```\n\n#### useStore: _function(): TStore_\n\n- Hook that subscribes to any change in the store\n\n```tsx\nconst CompStore = () =\u003e {\n  const store = useStore();\n\n  return \u003cdiv\u003e{JSON.stringify(store, null, 2)}\u003c/div\u003e;\n};\n```\n\n### createStoreContext\n\n#### useStore: _function(): TStore_\n\n- Hook that subscribes to any change in the store\n\n```tsx\nconst CompStore = () =\u003e {\n  const store = useStore();\n\n  return \u003cdiv\u003e{JSON.stringify(store, null, 2)}\u003c/div\u003e;\n};\n```\n\n#### useProduce: _function(): { produce, asyncProduce }_\n\n- Hook that gives an object containing the same functions [produce](#produce-functiondraft--void--tstore-tstore) and [asyncProduce](#asyncproduce-functionasync-draft--void--tstore-promise-tstore) from [createStore](#createstore)\n\n```tsx\nconst IncrementComp = () =\u003e {\n  const { produce } = useProduce();\n\n  return (\n    \u003cbutton\n      onClick={() =\u003e\n        produce((draft) =\u003e {\n          draft.count += 1;\n        })\n      }\n    \u003e\n      Increment\n    \u003c/button\u003e\n  );\n};\n```\n\n## Custom API\n\nThis is where this library aims to work the best using **type inference**, **memoization** and **mutability _with_ immutability** seemlessly without any boilerplate needed.\n\n### Custom Hooks\n\nIn both **createStore** and **createStoreContext** the functionality is **the same**.\n\nYou should specify an object inside the options object _(**second parameter**)_ called **hooks**.\n\nInside this object you have to follow the [**custom hooks naming rule**](https://reactjs.org/docs/hooks-custom.html#extracting-a-custom-hook) for every custom hook, and inside, you give a function that will receive **two parameters**, the first one will be the **state** of the store, and the second one will be the _optional_ **custom props** of the hook.\n\nIn the resulting store object you will get an object field called **hooks**, which will have all the custom hooks specified in the creation.\n\n```tsx\n// const ABStore = createStoreContext(\nconst ABStore = createStore(\n  { a: 1, b: 2 },\n  {\n    hooks: {\n      useA: ({ a, b }) =\u003e {\n        return a;\n      },\n      useB: ({ a, b }) =\u003e {\n        return b;\n      },\n      useMultiplyAxN: (\n        { a, b },\n        n: number\n        /* Only if you are using TypeScript \n      you have to specify the type of the props */\n      ) =\u003e {\n        return a * n;\n      },\n    },\n  }\n);\n\nconst { useA, useB } = ABStore.hooks;\n// You can destructure the hooks if you want\n\nconst A = () =\u003e {\n  const a = useA();\n\n  return \u003cp\u003e{a}\u003c/p\u003e;\n};\nconst B = () =\u003e {\n  const b = useB();\n\n  return \u003cp\u003e{b}\u003c/p\u003e;\n};\nconst AxN = () =\u003e {\n  // Or you can just call the hook from\n  // the store object itself\n  const axn = ABStore.hooks.useMultiplyAxN(10);\n\n  return \u003cp\u003e{axn}\u003c/p\u003e;\n};\n```\n\n\u003e Check **https://pabloszx.github.io/react-state-selector** for more advanced usage, like giving **multiple props** to a custom hook or returning a **new instance of data** based on the state and props, all of those, efficiently.\n\n### Custom Actions\n\nA very important feature of any global state is being able to modify it based on arguments given to a function and/or based on the current state, and using a reducer and dispatching action types and payload is a possible solution, but in this library the proposed solution is to specify the action types **explicitly** in the function names and it's payload in it's arguments.\n\nIn both **createStore** and **createStoreContext** the functionality is the same, but the usage in **createStoreContext** is a bit different due to **React Context API** constraints.\n\nYou should specify an object inside the options object _(**second parameter**)_ called **actions** and/or **asyncActions**.\n\n#### Actions\n\nInside the **actions** object you have to give functions called whatever you want, which will receive the custom arguments of the action, and this function should **return another function** which will receive the **state draft**, and that one should either return nothing or a new instance of the store state, just like [produce](#produce-functiondraft--void--tstore-tstore).\n\nThe resulting object store will have either:\n\n- **actions** object field in **createStore**.\n- **useActions** hook that returns the custom actions in **createStoreContext**\n\n```tsx\nconst Store = createStore(\n  { a: 1 },\n  {\n    actions: {\n      increment: (n: number) =\u003e (draft) =\u003e {\n        draft.a += n;\n      },\n    },\n  }\n);\nconst StoreCtx = createStore(\n  { b: 1 },\n  {\n    actions: {\n      decrement: (n: number) =\u003e (draft) =\u003e {\n        draft.b -= n;\n      },\n    },\n  }\n);\nconst A = () =\u003e {\n  const { a } = Store.useStore();\n\n  return (\n    \u003cdiv\u003e\n      \u003cbutton onClick={() =\u003e Store.increment(5)}\u003eIncrement\u003c/button\u003e\n      \u003cp\u003e{a}\u003c/p\u003e\n    \u003c/div\u003e\n  );\n};\nconst B = () =\u003e {\n  const { b } = StoreCtx.useStore();\n  const { decrement } = StoreCtx.useActions();\n\n  return (\n    \u003cdiv\u003e\n      \u003cbutton onClick={() =\u003e decrement(5)}\u003eDecrement\u003c/button\u003e\n      \u003cp\u003e{b}\u003c/p\u003e\n    \u003c/div\u003e\n  );\n};\n```\n\n#### Async Actions\n\nAsync actions need to be defined in another object inside the **options object** called **asyncActions**, and the first function should **not** be _async_ itself since it receives a **_dispatch like_** function which works just like [produce](#produce-functiondraft--void--tstore-tstore), and after that you should define the **async function** which will receive the parameters you expect in the action.\n\nThe async actions are put separately in an **asyncActions object** `or useAsyncActions() hook`.\n\n```tsx\nenum State {\n  waiting,\n  loading,\n  complete,\n  error,\n}\n\n// const Store = createStoreContext(\nconst Store = createStore(\n  {\n    data: undefined,\n    state: State.waiting,\n  },\n  {\n    asyncActions: {\n      getData: (produce) =\u003e async (bodyArgs) =\u003e {\n        produce((draft) =\u003e {\n          draft.state = State.loading;\n        });\n\n        try {\n          const response = await axios.post(\"/data\", bodyArgs);\n\n          produce((draft) =\u003e {\n            draft.state = State.complete;\n            draft.data = response.data;\n          });\n        } catch (err) {\n          console.error(err);\n          produce((draft) =\u003e {\n            draft.state = State.error;\n          });\n        }\n      },\n    },\n  }\n);\n\nconst Data = () =\u003e {\n  const storeData = Store.useStore();\n\n  // const {getData} = Store.useAsyncActions();\n  const { getData } = Store.asyncActions;\n\n  switch (storeData.state) {\n    case State.loading:\n      return \u003cp\u003eLoading...\u003c/p\u003e;\n    case State.complete:\n      return \u003cp\u003e{JSON.stringify(storeData.data)}\u003c/p\u003e;\n    case State.waiting:\n      return (\n        \u003cbutton\n          onClick={async () =\u003e {\n            const newStore = await getData();\n            console.log(\"newStore\", newStore);\n          }}\n        \u003e\n          Get Data\n        \u003c/button\u003e\n      );\n    case State.error:\n    default:\n      return \u003cp\u003eERROR! Check the console\u003c/p\u003e;\n  }\n};\n```\n\n\u003e Keep in mind that you can mix common synchronous actions and async actions in a single store, but you should not repeat the action names in both objects.\n\n## **localStorage** data persistence and **DevTools**\n\nWhen creating an store via **createStore** or **createStoreContext** you can specify some fields that enable some useful features:\n\n```tsx\n//createStoreContext(\ncreateStore(\n  {\n    foo: \"bar\",\n  },\n  {\n    /**\n     * devName\n     *\n     * Activates the Redux DevTools for this store using\n     * this name as reference.\n     */\n    devName: \"fooBarStore\",\n\n    /**\n     * devToolsInProduction\n     *\n     * Activates the Redux Devtools functionality in production.\n     *\n     * By default this is false\n     */\n    devToolsInProduction: true,\n    storagePersistence: {\n      /**\n       * isActive\n       *\n       * Activates the data persistence in this store\n       **/\n      isActive: true,\n      /**\n       * persistenceKey\n       *\n       * Set the key used for the storage persistence method.\n       *\n       * It has to be a string, and if it's not specified\n       * reuses the \"devName\", but it has to exists at least one\n       * of these two if the storagePersistence is active\n       **/\n      persistenceKey: \"fooBarStore\",\n      /**\n       * debounceWait\n       *\n       * Calling an external store like localStorage every time\n       * any change is made to the store is computationally expensive,\n       * and that's why by default this functionality is being debounced\n       * to be called only when needed, after X amount of milliseconds\n       * since the last change to the store.\n       *\n       * By default it's set to 3000 ms, but you can customize it to\n       * be 0 if you want almost instant save to the persistence store\n       **/\n      debounceWait: 1000,\n      /**\n       * persistenceMethod\n       *\n       * You also can customize the persistence method,\n       * but by default uses window.localStorage.\n       *\n       * Keep in mind that it should follow the same API\n       * of setItem and getItem of localStorage\n       **/\n      persistenceMethod: window.localStorage,\n      /**\n       * isSSR\n       *\n       * Flag used to specify that this store is going to be\n       * used in server side rendering environments and it prevents\n       * client/server mismatched html on client side hydration\n       *\n       * false by default\n       **/\n      isSSR: true,\n    },\n  }\n);\n```\n\n## **Map, Set, old browsers, React Native** support\n\nIn [Immer](https://immerjs.github.io/immer) latest version in order to reduce bundle size if you need support for **Map**, **Set**, **old browsers** or **React Native** you need to call some specific Immer functions as early as possible in your application.\n\n```tsx\n// You can import from either immer or react-state-selector\n\n// import { enableES5, enableMapSet } from \"immer\";\nimport { enableES5, enableMapSet } from \"react-state-selector\";\n\n// Import and call as needed\n\nenableES5();\n\nenableMapSet();\n```\n\n\u003e [Immer documentation](https://immerjs.github.io/immer/docs/installation#pick-your-immer-version)\n\n---\n\n\u003e Heavily inspired by [redux](https://github.com/reduxjs/redux) and [react-sweet-state](https://github.com/atlassian/react-sweet-state)\n\n---\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FPabloSzx%2Freact-state-selector","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FPabloSzx%2Freact-state-selector","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FPabloSzx%2Freact-state-selector/lists"}