{"id":19152325,"url":"https://github.com/nanostores/query","last_synced_at":"2025-05-15T10:03:22.478Z","repository":{"id":92271739,"uuid":"583016559","full_name":"nanostores/query","owner":"nanostores","description":"⚡️ Powerful data fetching library for Nano Stores. TS/JS. Framework agnostic.","archived":false,"fork":false,"pushed_at":"2025-01-13T11:33:48.000Z","size":329,"stargazers_count":261,"open_issues_count":17,"forks_count":15,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-05-15T10:03:16.146Z","etag":null,"topics":["async","cache","fetch","graphql","nano","query","stale","stale-while-revalidate"],"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/nanostores.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":"2022-12-28T14:14:49.000Z","updated_at":"2025-05-14T01:59:58.000Z","dependencies_parsed_at":"2024-04-09T12:57:13.312Z","dependency_job_id":"820bb71a-edd1-4ac4-9607-043a400cd574","html_url":"https://github.com/nanostores/query","commit_stats":{"total_commits":42,"total_committers":1,"mean_commits":42.0,"dds":0.0,"last_synced_commit":"95e17c98074eb57f8ee5f2a5bd58d3d73923f862"},"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nanostores%2Fquery","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nanostores%2Fquery/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nanostores%2Fquery/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nanostores%2Fquery/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nanostores","download_url":"https://codeload.github.com/nanostores/query/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254319716,"owners_count":22051072,"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":["async","cache","fetch","graphql","nano","query","stale","stale-while-revalidate"],"created_at":"2024-11-09T08:17:28.336Z","updated_at":"2025-05-15T10:03:17.443Z","avatar_url":"https://github.com/nanostores.png","language":"TypeScript","readme":"# Nano Stores Query\n\n\u003cimg align=\"right\" width=\"92\" height=\"92\" title=\"Nano Stores logo\"\n     src=\"https://nanostores.github.io/nanostores/logo.svg\"\u003e\n\nA tiny data fetcher for [Nano Stores](https://github.com/nanostores/nanostores).\n\n- **Small**. 1.8 Kb (minified and gzipped).\n- **Familiar DX**. If you've used [`swr`](https://swr.vercel.app/) or [`react-query`](https://react-query-v3.tanstack.com/), you'll get the same treatment, but for 10-20% of the size.\n- **Built-in cache**. `stale-while-revalidate` caching from  [HTTP RFC 5861](https://tools.ietf.org/html/rfc5861). User rarely sees unnecessary loaders or stale data.\n- **Revalidate cache**. Automaticallty revalidate on interval, refocus, network  recovery. Or just revalidate it manually.\n- **Nano Stores first**. Finally, fetching logic *outside* of components. Plays nicely with [store events](https://github.com/nanostores/nanostores#store-events), [computed stores](https://github.com/nanostores/nanostores#computed-stores), [router](https://github.com/nanostores/router), and the rest.\n- **Transport agnostic**. Use GraphQL, REST codegen, plain fetch or anything, that returns Promises (Web Workers, SubtleCrypto, calls to WASM, etc.).\n\n\u003ca href=\"https://evilmartians.com/?utm_source=nanostores-query\"\u003e\n  \u003cimg src=\"https://evilmartians.com/badges/sponsored-by-evil-martians.svg\"\n       alt=\"Sponsored by Evil Martians\" width=\"236\" height=\"54\"\u003e\n\u003c/a\u003e\n\n## Install\n\n```sh\nnpm install nanostores @nanostores/query\n```\n\n## Usage\n\nSee [Nano Stores docs](https://github.com/nanostores/nanostores#guide) about using the store and subscribing to store’s changes in UI frameworks.\n\n### Context\n\nFirst, we define the context. It allows us to share the default fetcher implementation and general settings between all fetcher stores, and allows for simple mocking in tests and stories.\n\n```ts\n// store/fetcher.ts\nimport { nanoquery } from '@nanostores/query';\n\nexport const [createFetcherStore, createMutatorStore] = nanoquery({\n  fetcher: (...keys) =\u003e fetch(keys.join('')).then((r) =\u003e r.json()),\n});\n```\n\nSecond, we create the fetcher store. `createFetcherStore` returns the usual `atom()` from Nano Stores, that is reactively connected to all stores passed as keys. Whenever the `$currentPostId` updates, `$currentPost` will call the fetcher once again.\n\n```ts\n// store/posts.ts\nimport { createFetcherStore } from './fetcher';\n\nexport const $currentPostId = atom('');\nexport const $currentPost = createFetcherStore\u003cPost\u003e(['/api/post/', $currentPostId]);\n```\n\nThird, just use it in your components. `createFetcherStore` returns the usual `atom()` from Nano Stores.\n\n```tsx\n// components/Post.tsx\nconst Post = () =\u003e {\n  const { data, loading } = useStore($currentPost);\n\n  if (data) return \u003cdiv\u003e{data.content}\u003c/div\u003e;\n  if (loading) return \u003c\u003eLoading...\u003c/\u003e;\n  \n  return \u003c\u003eError!\u003c/\u003e;\n};\n\n```\n\n## `createFetcherStore`\n\n```ts\nexport const $currentPost = createFetcherStore\u003cPost\u003e(['/api/post/', $currentPostId]);\n```\n\nIt accepts two arguments: **key input** and **fetcher options**.\n\n```ts\ntype NoKey = null | undefined | void | false;\ntype SomeKey = string | number | true;\n\ntype KeyInput = SomeKey | Array\u003cSomeKey | ReadableAtom\u003cSomeKey | NoKey\u003e | FetcherStore\u003e;\n```\n\nUnder the hood, nanoquery will get the `SomeKey` values and pass them to your fetcher like this: `fetcher(...keyParts)`. Few things to notice:\n\n- if any atom value is either `NoKey`, we never call the fetcher—this is the conditional fetching technique we have;\n- if you had `SomeKey` and then transitioned to `NoKey`, store's `data` will be also unset;\n- you can, in fact, pass another fetcher store as a dependency! It's extremely useful, when you need to create reactive chains of requests that execute one after another, but only when previous one was successful. In this case, if this fetcher store has loaded its data, its key part will be the concatenated `key` of the store. [See this example](https://stackblitz.com/edit/react-ts-9rr8p8?file=App.tsx).\n\n```ts\ntype Options = {\n  // The async function that actually returns the data\n  fetcher?: (...keyParts: SomeKey[]) =\u003e Promise\u003cunknown\u003e;\n  // How much time should pass between running fetcher for the exact same key parts\n  // default = 4000 (=4 seconds; provide all time in milliseconds)\n  dedupeTime?: number;\n  // Lifetime for the stale cache. If present, stale cache will be shown to a user.\n  // Cannot be less than `dedupeTime`.\n  // default = Infinity\n  cacheLifetime?: number;\n  // If we should revalidate the data when the window focuses\n  // default = false\n  revalidateOnFocus?: boolean;\n  // If we should revalidate the data when network connection restores\n  // default = false\n  revalidateOnReconnect?: boolean;\n  // If we should run revalidation on an interval\n  // default = 0, no interval\n  revalidateInterval?: number;\n  // Error handling for specific fetcher store. Will get whatever fetcher function threw\n  onError?: (error: any) =\u003e void;\n  // A function that defines a timeout for automatic invalidation in case of an error\n  // default — set to exponential backoff strategy\n  onErrorRetry?: OnErrorRetry | null;\n}\n```\n\nThe same options can be set on the context level where you actually get the\n`createFetcherStore`.\n\n## `createMutatorStore`\n\nMutator basically allows for 2 main things: tell nanoquery **what data should be revalidated** and **optimistically change data**. From interface point of view it's essentially a wrapper around your async function with some added functions.\n\nIt gets an object with 3 arguments:\n\n- `data` is the data you pass to the `mutate` function;\n- `invalidate` and `revalidate`; more on them in section [How cache works](#how-cache-works)\n- `getCacheUpdater` allows you to get current cache value by key and update it with\na new value. The key is also revalidated by default.\n\n```ts\nexport const $addComment = createMutatorStore\u003cComment\u003e(\n  async ({ data: comment, revalidate, getCacheUpdater }) =\u003e {\n    // You can either revalidate the author…\n    revalidate(`/api/users/${comment.authorId}`);\n\n    // …or you can optimistically update current cache.\n    const [updateCache, post] = getCacheUpdater(`/api/post/${comment.postId}`);\n    updateCache({ ...post, comments: [...post.comments, comment] });\n\n    // Even though `fetch` is called after calling `revalidate`, we will only\n    // revalidate the keys after `fetch` resolves\n    return fetch('…')\n  }\n);\n```\n\nThe usage in component is very simple as well:\n\n```tsx\nconst AddCommentForm = () =\u003e {\n  const { mutate, loading, error } = useStore($addComment);\n\n  return (\n    \u003cform\n      onSubmit={(e) =\u003e {\n        e.preventDefault();\n        mutate({ postId: \"…\", text: \"…\" });\n      }}\n    \u003e\n      \u003cbutton disabled={loading}\u003eSend comment\u003c/button\u003e\n      {error \u0026\u0026 \u003cp\u003eSome error happened!\u003c/p\u003e}\n    \u003c/form\u003e\n  );\n};\n```\n\n`createMutatorStore` accepts an optional second argument with settings: \n\n```ts\ntype MutationOptions = {\n  // Error handling for specific fetcher store. Will get whatever mutation function threw\n  onError?: (error: any) =\u003e void;\n  // Throttles all subsequent calls to `mutate` function until the first call finishes.\n  // default: true\n  throttleCalls?: boolean;\n}\n```\n\nYou can also access the mutator function via `$addComment.mutate`—the function is the same.\n\n## _Third returned item_\n\n(we didn't come up with a name for it 😅)\n\n`nanoquery` function returns a third item that gives you a bit more manual control over the behavior of the cache.\n\n```ts\n// store/fetcher.ts\nimport { nanoquery } from '@nanostores/query';\n\nexport const [,, { invalidateKeys, revalidateKeys, mutateCache }] = nanoquery();\n```\n\nBoth `invalidateKeys` and `revalidateKeys` accept one argument—the keys—in 3 different forms, that we call _key selector_. More on them in section [How cache works](#how-cache-works)\n\n```ts\n// Single key\ninvalidateKeys(\"/api/whoAmI\");\n// Array of keys\ninvalidateKeys([\"/api/dashboard\", \"/api/projects\"]);\n/**\n * A function that will be called against all keys in cache.\n * Must return `true` if key should be invalidated.\n */\ninvalidateKeys((key) =\u003e key.startsWith(\"/api/job\"));\n```\n\n`mutateCache` does one thing only: it mutates cache for those keys and refreshes all fetcher stores that have those keys currently.\n\n```ts\n/**\n * Accepts key in the same form as `invalidateKeys`: single, array and a function.\n */\nmutateCache((key) =\u003e key === \"/api/whoAmI\", { title: \"I'm Batman!\" });\n```\n\nKeep in mind: we're talking about the serialized singular form of keys here. You cannot pass stuff like `['/api', '/v1', $someStore]`, it needs to be the full key in its string form.\n\n## Recipes\n\n### How cache works\n\nAll of this is based on [`stale-while-revalidate`](https://tools.ietf.org/html/rfc5861) methodology. The goal is simple:\n\n1. user visits `page 1` that fetches `/api/data/1`;\n2. user visits `page 2` that fetches `/api/data/2`;\n3. almost immediately user goes back to `page 1`. Instead of showing a spinner and loading data once again, we fetch it from cache.\n\nSo, using this example, let's try to explain different cache-related settings the library has:\n\n- `dedupeTime` is the time that user needs to spend on `page 2` before going back for the library to trigger fetch function once again.\n- `cacheLifetime` is the maximum possible time between first visit and second visit to `page 1` after which we will stop serving stale cache to user (so they will immediately see a spinner).\n- `revalidate` forces the `dedupeTime` for this key to be 0, meaning, the very next time anything can trigger fetch (e.g., `refetchOnInterval`), it will call fetch function. If you were on the page during revalidation, you'd see cached value during loading.\n- `invalidate` kills this cache value entirely—it's as if you never were on this page. If you were on the page during invalidation, you'd see a spinner immediately.\n\nSo, the best UI, we think, comes from this snippet:\n\n```tsx\n// components/Post.tsx\nconst Post = () =\u003e {\n  const { data, loading } = useStore($currentPost);\n\n  if (data) return \u003cdiv\u003e{data.content}\u003c/div\u003e;\n  if (loading) return \u003c\u003eLoading...\u003c/\u003e;\n  \n  return \u003c\u003eError!\u003c/\u003e;\n};\n```\n\nThis way you actually embrace the stale-while-revalidate concept and only show spinners when there's no cache, but other than that you always fall back to cached state.\n\n### Local state and Pagination\n\nAll examples above use module-scoped stores, therefore they can only have a single\ndata point stored. But what if you need, say, a store that fetches data based on\ncomponent state? Nano Stores do not limit you in any way, you can easily achieve\nthis by creating a store instance limited to a single component:\n\n```tsx\nconst createStore = (id: string) =\u003e () =\u003e\n  createFetcherStore\u003c{ avatarUrl: string }\u003e(`/api/user/${id}`);\n\nconst UserAvatar: FC\u003c{ id: string }\u003e = ({ id }) =\u003e {\n  const [$user] = useState(createStore(id));\n\n  const { data } = useStore($user);\n  if (!data) return null;\n\n  return \u003cimg src={data.avatarUrl} /\u003e;\n};\n```\n\nThis way you can leverage all nanoquery features, like cache or refetching, but\nnot give up the flexibility of component-level data fetching.\n\n### Refetching and manual mutation\n\nWe've already walked through all the primitives needed for refetching and mutation, but the interface is rather bizarre with all those string-based keys. Often all we actually want is to refetch _current_ key (say, you have this refresh button in the UI), or mutate _current_ key, right?\n\nFor these cases we have 3 additional things on fetcher stores:\n\n1. `fetcherStore.invalidate` and `fetcherStore.revalidate`\n2. `fetcherStore.mutate`. It's a function that mutates current key for the fetcher. Accepts the new value.\n3. `fetcherStore.key`. Well, it holds current key in serialized form (as a string).\n\nTypically, those 3 are more than enough to make all look very good.\n\n### Lazy fetcher\n\nSometimes you don't want a store, you just want an async function that's gonna handle the errors and leverage the cache (perform cache lookup, save data in there upon successful execution, etc.).\n\nFor that case use `fetcherStore.fetch` function. It will always resolve with the same data type as store itself (`error` and `data` only).\n\nFew gotchas:\n\n- it will execute against currently set keys (no way to customize them for the call);\n- it will still leverage deduplication;\n- underlying fetcher function cannot resolve or reject with `undefined` as their value. This will lead to hanging promises.\n\n### Dependencies, but not in keys\n\nLet's say, you have a dependency for your fetcher, but you don't want it to be in your fetcher keys. For example, this could be your `userId`—that would be a hassle to put it _everywhere_, but you need it, because once you change your user, you don't want to have stale cache from the previous user.\n\nThe idea here is to wipe the cache manually. For something as big as a new refresh token you can go and do a simple \"wipe everything you find\":\n\n```ts\nonSet($refreshToken, () =\u003e invalidateKeys(() =\u003e true))\n```\n\nIf your store is somehow dependant on other store, but it shouldn't be reflected in the key, you should do the same, but more targetly:\n\n```ts\nonSet($someOutsideFactor, $specificStore.invalidate)\n```\n\n### Error handling\n\n`nanoquery`, `createFetcherStore` and `createMutationStore` all accept an optional setting called `onError`. Global `onError` handler is called for all errors thrown from fetcher and mutation calls unless you set a local `onError` handler for a specific store (then it \"overwrites\" the global one).\n\n`nanoquery` and `createFetcherStore` both accept and argument `onErrorRetry`. It also cascades down from context to each fetcher and can be rewritten by a fetcher. By default it implements an exponential backoff strategy with an element of randomness, but you can set your own according to `OnErrorRetry` signature. If you want to disable automatic revalidation for error responses, set this value to `null`.\n\nThis feature is particularly handy for stuff like showing flash notifications for all errors.\n\n`onError` gets a single argument of whatever the fetch or mutate functions threw.\n\n### React Native\n\nReact Native is fully supported. For `revalidateOnReconnect` to work, you need to install `@react-native-community/netinfo` package. It's optional: if you don't `reconnect` just won't trigger revalidation. The rest works as usual.\n\nIf you use [package exports](https://reactnative.dev/blog/2023/06/21/package-exports-support#enabling-package-exports-beta), you can import the library as usual. Otherwise, do this:\n\n```ts\nimport { nanoquery } from \"@nanostores/query/react-native\";\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnanostores%2Fquery","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnanostores%2Fquery","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnanostores%2Fquery/lists"}