{"id":21347309,"url":"https://github.com/ziolko/active-store","last_synced_at":"2025-07-12T17:31:55.756Z","repository":{"id":257550463,"uuid":"685687898","full_name":"ziolko/active-store","owner":"ziolko","description":"New kind of state library for React that heavily utilizes the React Suspense API.","archived":false,"fork":false,"pushed_at":"2024-12-21T21:04:03.000Z","size":375,"stargazers_count":18,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-06T16:53:27.830Z","etag":null,"topics":["react","reactjs","state-management"],"latest_commit_sha":null,"homepage":"","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/ziolko.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,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-08-31T19:29:31.000Z","updated_at":"2024-12-21T21:04:07.000Z","dependencies_parsed_at":"2024-10-31T15:34:05.608Z","dependency_job_id":null,"html_url":"https://github.com/ziolko/active-store","commit_stats":null,"previous_names":["ziolko/active-store"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ziolko/active-store","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ziolko%2Factive-store","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ziolko%2Factive-store/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ziolko%2Factive-store/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ziolko%2Factive-store/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ziolko","download_url":"https://codeload.github.com/ziolko/active-store/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ziolko%2Factive-store/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264087401,"owners_count":23555433,"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":["react","reactjs","state-management"],"created_at":"2024-11-22T02:13:40.566Z","updated_at":"2025-07-12T17:31:55.734Z","avatar_url":"https://github.com/ziolko.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"Active Store is a new state library for React that heavily utilizes the React Suspense API.\n\n## Quick start\n\nTo get started install the npm package with `npm i active-store`. You can then create a store as shown below:\n\n```typescript\nimport { createContext, useContext } from \"react\";\nimport { activeState, activeAsync, activeComputed } from \"active-store\";\n\nfunction activeAppStore() {\n  // activeState is the simplest building block of active store.\n  // It keeps a piece of state and updates UI when the state changes\n  // You can later access run:\n  // - userLogin.get() to get the current value of the active state\n  // - userLogin.set(\"new-value\") to change the value stored in states\n  const userLogin = activeState(\"ziolko\");\n\n  // activeAsync is similar to useQuery in react-query - it fetches data\n  // for every unique set of arguments. In the case below there's only\n  // one argument called - login.\n  const githubProfile = activeAsync(\n    (\n      login: string\n    ): Promise\u003c{\n      id: number;\n      login: string;\n      avatar_url: string;\n      name: string;\n    }\u003e =\u003e\n      fetch(`https://api.github.com/users/${encodeURIComponent(login)}`).then(\n        (x) =\u003e x.json()\n      )\n  );\n\n  // activeComputed allows to get a computed value based on\n  // activeState and activeAsync. It uses React Suspense api to wait\n  // for activeAsync to resolve\n  const profile = activeComputed(() =\u003e {\n    // get currently selected github login. You will see a lot of `.get()`\n    // in the codebase using active-store\n    const login = userLogin.get();\n\n    // start fetching data from activeAsync for currently\n    // selected github login. The line below will suspend until\n    // github profile finishes loading.\n    // There's no need for special handling of the async loading state.\n    return githubProfile.get(login);\n  });\n\n  // Of course you can combine computed values in activeComputed, too\n  const userName = activeComputed(() =\u003e profile.get().name);\n\n  return {\n    userLogin,\n    profile,\n    userName,\n  };\n}\n\nconst store = activeAppStore(); // Create an instance of the store\n\n// Define a helper hook 'useStore' to get an instance\n// of the store in your components\nconst storeContext = createContext\u003cReturnType\u003ctypeof activeAppStore\u003e\u003e(store);\nexport const useStore = () =\u003e useContext(storeContext);\n```\n\nYou are now all set to use the store in your app:\n\n```typescript\nimport \"./App.css\";\nimport { useStore } from \"./store\";\nimport { useActive, ActiveBoundary } from \"active-store\";\n\nexport default function App() {\n  return (\n    \u003cdiv\u003e\n      \u003cCurrentUserPicker /\u003e\n      {/*  \n        Active boundary wraps a section of the app that loads and fails\n        together \n        - fallback is used while data required to load child components\n      is loading. \n        - errorFallback is used when any of the data fails to load\n      with an exception\n      */}\n      \u003cActiveBoundary\n        fallback=\"Loading...\"\n        errorFallback=\"There was an error while loading the data :(\"\n      \u003e\n        \u003cGithubProfile /\u003e\n      \u003c/ActiveBoundary\u003e\n    \u003c/div\u003e\n  );\n}\n\nfunction CurrentUserPicker() {\n  const store = useStore();\n  const currentLogin = useActive(store.userLogin);\n  const options = [\"stephencelis\", \"lidel\", \"arogozhnikov\", \"ziolko\"];\n  return (\n    \u003c\u003e\n      {options.map((login) =\u003e (\n        \u003cbutton\n          key={login}\n          onClick={() =\u003e store.userLogin.set(login)}\n          style={{ background: currentLogin === login ? \"red\" : \"transparent\" }}\n        \u003e\n          {login}\n        \u003c/button\u003e\n      ))}\n    \u003c/\u003e\n  );\n}\n\nfunction GithubProfile() {\n  const store = useStore();\n\n  // React will suspend until the data is loaded\n  const profile = useActive(store.profile);\n\n  // React will suspend until the data is loaded\n  const name = useActive(store.userName);\n\n  return (\n    \u003cdiv\u003e\n      {name} \u003cimg src={profile.avatar_url} width={80} /\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n## Examples\n\n- [Simple HackerNews client](https://codesandbox.io/p/sandbox/headless-resonance-dfzgzw?file=%2Fsrc%2Fstore.ts)\n- [Simple store for a feedback form with a single input](https://github.com/roombelt/timeline/blob/main/src/store/feedback.ts)\n- [An example of a splitting a big store into several smaller stores](https://github.com/roombelt/timeline/blob/main/src/store/index.ts)\n- [Collection of active utilities (e.g. activeLocalStorage)](https://github.com/roombelt/timeline/blob/main/src/store/utils.ts)\n- [Custom utility activeDeferred acting like useDeferredValue](https://stackblitz.com/edit/vitejs-vite-f9y5fn?file=src%2FApp.tsx)\n\n## Reference\n\n### activeState\n\nThe simplest building block of the app state. It's like the `useState` hook.\n\n```typescript\nconst state = activeState\u003cT\u003e(initialState: T, options?: { \n  // onSubscribe is called when first subscriber subscribes to the state. \n  // The callback returned from onSubscribe is called when last subscriber unsubscribes\n  onSubscribe?: () =\u003e () =\u003e void;\n  // Number of miliseconds the data will be cached after \n  // last subscriber unsubscribes (defaults to infinity)\n  gcTime?: number;\n});\n\n\n// Returns the current value of the state\nstate.get();\n\n// Sets new value for the state. Trigers re-renders of\n// components that depend on it. Triggers invalidation\n// on activeComputed that depend on it.\nstate.set(newValue: T);\n\n// Manually subscribes to changes in the state.\n// Takes a listener as a parameter. Returns unsubscribe function.\nstate.subscribe(listener: () =\u003e any) =\u003e () =\u003e void;\n```\n\nAlternatively, you can provide a factory function as an `initialState`:\n```typescript\nconst greetings = activeState((name: string) =\u003e `Hello ${name}`, {\n  onSubscribe(name: string) {\n    console.log(`Subscribed to ${name}`);\n    return () =\u003e { console.log(`Unsubscribed from ${name}`); }\n  },\n});\n\nconsole.log(greetings.get('Adam')); // will print \"Hello Adam\"\n\n// Set greetings for \"Adam\" to \"Hi Adam\"\n// Notice that the value comes first, and key comes after it\ngreetings.set('Hi Adam', 'Adam');\n```\n\n### activeAsync\n\nThis is heavily based on React Query, so if you are familiar with this library you will feel like home.\n\n```typescript\n// Create an async state with a factor function returning promise.\n// Example: https://stackblitz.com/edit/vitejs-vite-mosens?file=src%2Fstore.ts\n// Important: The async state re-fetches based only on the provided parameters.\n// If you use e.g. activeState in the factory function, updating it's state\n// won't trigger a re-fetch.\nconst query = activeAsync(factory: (...args: P) =\u003e Promise\u003cR\u003e, options?: { \n  // Number of retries in case of failure\n  retry?: number | false;\n  // onSubscribe is called when first subscriber subscribes to the state. \n  // The callback returned from onSubscribe is called when last subscriber unsubscribes\n  onSubscribe?: (...args: P) =\u003e () =\u003e void;\n  // Number of miliseconds the data will be cached after \n  // last subscriber unsubscribes (defaults to infinity)\n  gcTime?: number;\n});\n\n\n// If the query for \"hello\" \"world\" has already resolved, returns the value.\n// If it rejected it throws the rejection reason as an exception\n// If query is pending, it throws a React Suspense error that\n// suspenses rendering React components and activeComputed\n// (more on this below).\nquery.get(\"hello\", \"world\");\n\n// Returns the current state of the query. The returned state\n// has the following fields that are very similar to react-query\n// - status: \"pending\" | \"success\" | \"error\";\n// - isPending: boolean;\n// - isSuccess: boolean;\n// - isError: boolean;\n// - isRefetching: boolean;\n// - isFetching: boolean;\n// - isStale: boolean;\n// - data?: R;\n// - error?: any;\n// - dataUpdatedAt?: number;\n// - errorUpdatedAt?: number;\n// - fetchStatus: \"fetching\" | \"paused\" | \"idle\";\nquery.state(\"hello\", \"world\");\n\n// Sets new value for the async state.\nstate.set(newValue: T);\n\n// Returns a promise for query for given parameters\nquery.getPromise(\"hello\" ,\"world\");\n\n// Prefetch data - use it if you're sure that the data will be needed soon\nquery.prefetch(\"hello\", \"world\") ;\n\n// Invalidate query for given parameters - will mark data as stale\n// and refetch if any component uses the query (either directly, or\n// through activeComputed)\nquery.invalidateOne(\"hello\", \"world\");\n\n// Invalidate query for all entries for which selector returns true.\n// This marks data as stale and refetch queries used in any visible\n// React component (either directly or through activeComputed)\n// Options:\n// - reset (default false) - reset the query to the initial state\n//                           (idle, with no data or error)\nquery.invalidate(\n  selector: (...args: P) =\u003e boolean,\n  options?: { reset?: boolean }\n);\n```\n\n### activeComputed\n\nCreates a computed state based on any other active state. If any React component is subscribed to it, it\nrecomputes automatically whenever any of its dependency changes.\n\n```typescript\n// The provided factory function must not be async\n// (or return a Promise) - TypeScript will complain when this happens.\n// You can use any combination of activeState, activeAsync, or\n// activeComputed inside.\nconst computed = activeComputed(factory: (...args: P) =\u003e R, options?: { \n  // Number of miliseconds the data will be cached after \n  // last subscriber unsubscribes (defaults to infinity)\n  gcTime?: number;\n});\n\n// As active computed can depend on active query, it has to\n// follow its async semantics:\n// - If the computed for \"hello\" \"world\" has already resolved,\n//   returns the value.\n// - If it rejected it throws the rejection reason\n//   as an exception\n// - If it's pending, it throws a React Suspense error that\n//   suspends rendering React components and activeComputed\n//   that depend on it (more on this below).\ncomputed.get(\"hello\", \"world\");\n\n// Returns the current state of the computed value.\n// The returned state has the following fields:\n// - status: \"pending\" | \"success\" | \"error\";\n// - data?\n// - error?\ncomputed.state(\"hello\", \"world\");\n\n// Prefetch data - use it if you're sure that the data will be needed soon\ncomputed.prefetch(\"hello\", \"world\");\n```\n\n### useActive\n\nIt's like `useSelector` from Redux. It connects your components with store.\n\n```typescript\n// Subscribes to the value of the activeComputed property.\n// Every time the value changes, component is re-rendered.\nconst value = useActive(() =\u003e store.computed.get(userId));\n\n// If `get` doesn't take any parameters, you can just pass\n// a reference to the getter:\nconst value = useActive(store.currentUser.get);\n\n// For convenience you can skip the `.get` part and\n// `useActive` will call it automatically:\nconst value = useActive(store.currentUser);\n```\n\n### getActive\n\nCompute value of an expression. If any active query or active computed is pending,\nit will wait until it fully resolves\n\n```typescript\nconst query = activeAsync(\n  () =\u003e new Promise((res) =\u003e setTimeout(() =\u003e res(\"Hello\"), 1000))\n);\n\n// Will await 1s until query resolves. \n// Returned value will be equal \"Hello World\"\nconst value = await getActive(() =\u003e `${query.get()} World` ); \n```\n\n### ActiveBoundary\n\nActive boundary wraps a section of the app that loads and fails together.\nIt's basically `\u003cSuspense\u003e` and React error boundary in a single component.\n\n- fallback is used while data required to load child components is loading.\n- errorFallback is used when any of the data fails to load with an exception\n\nYou can find an example of using it in the \"Quick start\" section.\n\n## How does suspending activeAsync and activeComputed work\n\nThis library uses the [React Suspense API](https://react.dev/reference/react/Suspense) for handling loading state.\nUnder the hood it (ab)uses exceptions.\n\nWhen `activeComputed` tries to get value of an active query (e.g. `user.get('mateusz')`) that is\ncurrently in a pending state, an special kind of exception is thrown. The trick is that the\nexception is _also_ a Promise.\n\nWhen the promise resolves, React re-renders the component so `activeComputed` recomputes the value and this time\nthe query is already resolved so it successfully computes the value.\n\n## License\n\nThe project is licensed under the MIT permissive license\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fziolko%2Factive-store","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fziolko%2Factive-store","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fziolko%2Factive-store/lists"}