All questions

Mini React Query Core

Premium

Mini React Query Core

A query client is the coordination layer under a data-fetching library: it caches server data by key, knows when that data is stale, collapses concurrent requests for the same key into one, and pushes updates to whoever is watching. You are going to build a minimal one — the core that a library like TanStack Query (React Query) wraps hooks around.

miniReactQueryCore() returns a client with four methods: fetchQuery to read data (from cache when fresh, from the network otherwise), getQueryData to peek at the cache, subscribe to be notified when a key's data changes, and invalidate to force a key to refetch. The interesting part is how these cooperate: a burst of fetchQuery calls must share one request, a fresh cache must skip the fetcher entirely, and an invalidation must refresh every live subscriber.

The whole thing is keyed by a string you choose, and each key runs independently — its own cached value, its own in-flight request, its own subscribers. A failed fetch is never written to the cache, so the next call for that key gets a clean retry rather than a stored error. Getting the ordering of the checks right — dedupe, then freshness, then fetch — is what separates a working client from one that either over-fetches or serves stale data.

Signature

function miniReactQueryCore(): QueryClient;

interface QueryClient {
  // Resolve data for `key`. Serve the cache when it is still fresh
  // (Date.now() - updatedAt < staleTime); otherwise call fetcher() once,
  // sharing one in-flight promise across concurrent callers, then cache
  // { data, updatedAt } and notify subscribers.
  fetchQuery<T>(
    key: string,
    fetcher: () => Promise<T>,
    options?: { staleTime?: number }, // default 0
  ): Promise<T>;

  // The cached data for `key`, or undefined if nothing is cached yet.
  getQueryData(key: string): unknown;

  // Register cb, called with the new data on every change for `key`.
  // Returns an unsubscribe function.
  subscribe(key: string, cb: (data: unknown) => void): () => void;

  // Mark `key` stale so the next fetchQuery refetches regardless of
  // staleTime; if `key` has subscribers, refetch and notify them now.
  invalidate(key: string): void;
}

Examples

const client = miniReactQueryCore();
const getUser = () => fetch('/api/user').then((r) => r.json());

// Two components mount at once and both ask for the user.
const a = client.fetchQuery('user', getUser);
const b = client.fetchQuery('user', getUser);
a === b; // true — getUser ran once; both callers await the same request

await a;
client.getQueryData('user'); // { name: 'Ada' } — now cached
const client = miniReactQueryCore();

// Fresh for 5s: a second call inside the window skips the fetcher.
await client.fetchQuery('user', getUser, { staleTime: 5000 });
await client.fetchQuery('user', getUser, { staleTime: 5000 }); // cache hit, no request

const off = client.subscribe('user', (data) => render(data));
client.invalidate('user'); // refetches now, then calls render(freshData)
off();                      // stop listening

Notes

  • fetchQuery always returns a promise — a fresh cache hit resolves with the stored data without touching the fetcher; a miss or a stale entry runs the fetcher once and resolves with its result.
  • staleTime is a freshness window, not an eviction timer — data is never thrown away. staleTime only decides whether the next fetchQuery reuses the cache or refetches. staleTime: 0 (the default) means always refetch.
  • Concurrent same-key calls share one promise — while a fetch is in flight, extra fetchQuery calls for that key receive the same pending promise, so the fetcher runs exactly once per flight.
  • Subscribers fire on change, not on subscribe — a callback runs when a fetch, or an invalidation-triggered refetch, writes new data for its key. The returned function removes it.
  • invalidate reuses the last fetcher — it refetches with whatever fetcher that key was most recently fetched with, so it can only refresh a key that has been fetched at least once.
  • Use Date.now() for staleness — compare Date.now() - entry.updatedAt against staleTime. You do not need high-resolution timers, and you do not need to cancel or debounce anything.

FAQ

How is this different from plain caching or memoization?
A memo or cache keys a value by input and reuses it. A query client adds the coordination around that value: a staleTime freshness window, de-duplication of concurrent fetches for the same key, change notifications to subscribers, and manual invalidation. It is the layer React Query provides on top of a store.
What does staleTime = 0 mean?
Zero means every entry is considered stale the instant it is written, so fetchQuery always refetches. A positive staleTime serves the cached value until Date.now() - updatedAt reaches it. staleTime never deletes data; it only decides whether the next call refetches.
How can invalidate refetch when it only receives a key?
Each fetchQuery records the last fetcher it saw for that key on the cache entry, so invalidate reuses that stored fetcher to refetch. That is why an entry has to remember its fetcher, not just its data, and why invalidate can only refetch a key that was fetched at least once.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium