SkillAgentSearch skills...

Query

⚡️ Powerful data fetching library for Nano Stores. TS/JS. Framework agnostic.

Install / Use

/learn @nanostores/Query
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Nano Stores Query

<img align="right" width="92" height="92" title="Nano Stores logo" src="https://nanostores.github.io/nanostores/logo.svg">

A tiny data fetcher for Nano Stores.

  • Small. Less than 2 Kb (minified and gzipped).
  • Familiar DX. If you've used swr or react-query, you'll get the same treatment, but for 10-20% of the size.
  • Built-in cache. stale-while-revalidate caching from HTTP RFC 5861. User rarely sees unnecessary loaders or stale data.
  • Revalidate cache. Automatically revalidate on interval, refocus, network recovery. Or just revalidate it manually.
  • Nano Stores first. Finally, fetching logic outside of components. Plays nicely with store events, computed stores, router, and the rest.
  • Transport agnostic. Use GraphQL, REST codegen, plain fetch or anything, that returns Promises (Web Workers, SubtleCrypto, calls to WASM, etc.).
<a href="https://evilmartians.com/?utm_source=nanostores-query"> <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"> </a>

Install

npm install nanostores @nanostores/query

Usage

See Nano Stores docs about using the store and subscribing to store’s changes in UI frameworks.

Context

First, 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.

// store/fetcher.ts
import { nanoquery } from '@nanostores/query';

export const [createFetcherStore, createMutatorStore] = nanoquery({
  fetcher: (...keys) => fetch(keys.join('')).then((r) => r.json()),
});

Second, 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.

// store/posts.ts
import { createFetcherStore } from './fetcher';

export const $currentPostId = atom('');
export const $currentPost = createFetcherStore<Post>(['/api/post/', $currentPostId]);

Third, just use it in your components. createFetcherStore returns the usual atom() from Nano Stores.

Note: before any component subscribes to the store, its value is { loading: false } with no data or error. Once a component subscribes (via useStore), the fetcher fires and loading becomes true. Always check data first to handle this correctly.

// components/Post.tsx
const Post = () => {
  const { data, error, loading } = useStore($currentPost);

  if (data) return <div>{data.content}</div>;
  if (error) return <>Failed to load</>;
  if (loading) return <>Loading...</>;

  return null;
};

createFetcherStore

export const $currentPost = createFetcherStore<Post>(['/api/post/', $currentPostId]);

It accepts two arguments: key input and fetcher options.

type NoKey = null | undefined | void | false;
type SomeKey = string | number | true;

type KeyInput = SomeKey | Array<SomeKey | ReadableAtom<SomeKey | NoKey> | FetcherStore>;

Under the hood, nanoquery will get the SomeKey values and pass them to your fetcher like this: fetcher(...keyParts). Few things to notice:

  • if any atom value is either NoKey, we never call the fetcher—this is the conditional fetching technique we have;
  • if you had SomeKey and then transitioned to NoKey, store's data will be also unset;
  • 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.
type Options = {
  // The async function that actually returns the data
  fetcher?: (...keyParts: SomeKey[]) => Promise<unknown>;
  // How much time should pass between running fetcher for the exact same key parts
  // default = 4000 (=4 seconds; provide all time in milliseconds)
  dedupeTime?: number;
  // Lifetime for the stale cache. If present, stale cache will be shown to a user.
  // Cannot be less than `dedupeTime`.
  // default = Infinity
  cacheLifetime?: number;
  // If we should revalidate the data when the window focuses
  // default = false
  revalidateOnFocus?: boolean;
  // If we should revalidate the data when network connection restores
  // default = false
  revalidateOnReconnect?: boolean;
  // If we should run revalidation on an interval
  // default = 0, no interval
  revalidateInterval?: number;
  // Error handling for specific fetcher store. Will get whatever fetcher function threw
  onError?: (error: any) => void;
  // A function that defines a timeout for automatic invalidation in case of an error
  // default — set to exponential backoff strategy
  onErrorRetry?: OnErrorRetry | null;
}

The same options can be set on the context level where you actually get the createFetcherStore.

createMutatorStore

Mutator 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.

It gets an object with 3 arguments:

  • data is the data you pass to the mutate function;
  • invalidate and revalidate; more on them in section How cache works
  • getCacheUpdater allows you to get current cache value by key and update it with a new value. The key is also revalidated by default.
export const $addComment = createMutatorStore<Comment>(
  async ({ data: comment, revalidate, getCacheUpdater }) => {
    // You can either revalidate the author…
    revalidate(`/api/users/${comment.authorId}`);

    // …or you can optimistically update current cache.
    const [updateCache, post] = getCacheUpdater(`/api/post/${comment.postId}`);
    updateCache({ ...post, comments: [...post.comments, comment] });

    // Even though `fetch` is called after calling `revalidate`, we will only
    // revalidate the keys after `fetch` resolves
    return fetch('…')
  }
);

The usage in component is very simple as well:

const AddCommentForm = () => {
  const { mutate, loading, error } = useStore($addComment);

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        mutate({ postId: "…", text: "…" });
      }}
    >
      <button disabled={loading}>Send comment</button>
      {error && <p>Some error happened!</p>}
    </form>
  );
};

createMutatorStore accepts an optional second argument with settings:

type MutationOptions = {
  // Error handling for specific fetcher store. Will get whatever mutation function threw
  onError?: (error: any) => void;
  // Throttles all subsequent calls to `mutate` function until the first call finishes.
  // default: true
  throttleCalls?: boolean;
}

You can also access the mutator function via $addComment.mutate—the function is the same.

Store states

Fetcher store

| State | loading | data | error | promise | When | |-------|-----------|--------|---------|-----------|------| | Initial (no subscribers) | false | — | — | — | Before any component subscribes | | Loading (no cache) | true | — | — | Promise | First fetch, or after invalidate | | Loading (stale cache) | true | Previous data | — | Promise | Refetch when cacheLifetime cache exists | | Success | false | T | — | — | Fetcher resolved | | Error | false | Previous data or — | E | — | Fetcher rejected | | Conditional fetch disabled | false | — | — | — | Any key part is null/undefined/false | | Deduplicated (from cache) | false | T | — | — | Same key fetched within dedupeTime |

Key behaviors:

  • invalidate wipes the cache entirely—the store goes back to "Loading (no cache)" with a spinner.
  • revalidate marks the cache as stale—the store shows "Loading (stale cache)" with the previous data visible.
  • When the store loses all subscribers (onStop), it resets to the initial state.

Mutator store

| State | loading | data | error | When | |-------|-----------|--------|---------|------| | Initial | false | — | — | Before mutate() is called | | Loading | true | — | — | mutate() called, awaiting result | | Success | false | Result | — | Mutation resolved | | Error | false | — | E | Mutation rejected |

Key behaviors:

  • mutate() always resets data and error before starting.
  • When throttleCalls is true (default), calling mutate() while already loading is a no-op.
  • When the store loses all subscribers, it resets to the initial state. Any in-flight mutation results are discarded.

Third returned item

(we didn't come up with a name for it 😅)

nanoquery function returns a third item that gives you a bit more manual control over the behavior of the cache.

// store/fetcher.ts
import { nanoquery } from '@nanostores/query';

export const [,, { invalidateKeys, revalidateKeys, mutateCache }] = nanoquery();

Both 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

// Single key
invalidateKeys("/api/whoAmI");
// Array of keys
invalidateKeys(["/api/dashboard", "/api/projects"]);
/**
 * A function that will be called against all keys in cach
View on GitHub
GitHub Stars326
CategoryDevelopment
Updated9d ago
Forks15

Languages

TypeScript

Security Score

100/100

Audited on Mar 22, 2026

No findings