javascript 138 lines · 4 tabs

Building a Minimal useQuery Hook with a Shared Cache Provider in React

Shared by codesnips Jul 2026
4 tabs
export class QueryCache {
  constructor() {
    this.entries = new Map();
  }

  getEntry(key) {
    if (!this.entries.has(key)) {
      this.entries.set(key, {
        status: 'idle',
        data: undefined,
        error: undefined,
        promise: null,
        listeners: new Set(),
      });
    }
    return this.entries.get(key);
  }

  subscribe(key, listener) {
    const entry = this.getEntry(key);
    entry.listeners.add(listener);
    return () => entry.listeners.delete(listener);
  }

  setEntry(key, patch) {
    const entry = this.getEntry(key);
    Object.assign(entry, patch);
    entry.listeners.forEach((fn) => fn());
  }

  fetchQuery(key, queryFn) {
    const entry = this.getEntry(key);
    if (entry.promise) return entry.promise;

    this.setEntry(key, { status: 'loading', error: undefined });
    const promise = Promise.resolve()
      .then(() => queryFn())
      .then((data) => {
        this.setEntry(key, { status: 'success', data, promise: null });
        return data;
      })
      .catch((error) => {
        this.setEntry(key, { status: 'error', error, promise: null });
        throw error;
      });

    entry.promise = promise;
    return promise;
  }
}
4 files · javascript Explain with highlit

This snippet implements a small useQuery-style data layer from scratch, showing how libraries like React Query or SWR work under the hood without the dependency. The core idea is a single in-memory cache keyed by a stable string, plus request deduplication so that N components asking for the same key trigger exactly one network call.

In QueryCache, the cache is a plain Map keyed by a serialized query key. Each entry holds the last data, an error, a status, and a set of subscriber callbacks. subscribe registers a listener and returns an unsubscribe function, mirroring the store pattern React's useSyncExternalStore expects. The fetchQuery method is where deduplication happens: an in-flight Promise is stashed on the entry so concurrent callers share it rather than firing duplicate requests. Results are written back with setEntry, which notifies every subscriber. This separates cache mechanics from React entirely, which makes the logic easy to reason about and test.

In QueryProvider, a single QueryCache instance is created once with useRef and handed down through context via QueryContext. Keeping the cache in a ref (not state) means the provider itself never re-renders when cache data changes — only the components subscribed to a given key do. useQueryClient is a thin accessor that throws when used outside the provider, catching a common wiring mistake early.

In useQuery hook, useSyncExternalStore binds a component to one cache entry. The subscribe callback filters by key so a component only re-renders when its own data changes, and getSnapshot reads the current entry. A useEffect kicks off fetchQuery on mount and whenever the key changes, adopting a stale-while-revalidate posture: cached data is returned immediately while a background refresh runs. The key is serialized with JSON.stringify so array or object keys stay stable across renders.

The main trade-offs are that this cache is memory-only with no eviction, TTL, or garbage collection, so long-lived apps would need those additions. It is ideal when a project wants deduped, shared, cache-backed fetching without pulling in a full data-fetching library.


Related snips

typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
javascript
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;

  setTimeout(() => {
    if (success) {

Promises and async/await patterns for asynchronous JavaScript

javascript promises async-await
by Alex Chang 1 tab
typescript
export type Settled<R> =
  | { status: 'fulfilled'; value: R }
  | { status: 'rejected'; reason: unknown };

export interface ConcurrencyOptions {
  limit: number;

Simple concurrency limiter for batch operations

node concurrency async
by codesnips 2 tabs
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["form"]
  static values = { delay: { type: Number, default: 250 } }

Debounced live search with Stimulus + Turbo Streams

rails hotwire stimulus
by codesnips 4 tabs
ruby
json.array! @posts do |post|
  json.cache! ['v1', post], expires_in: 1.hour do
    json.id post.id
    json.title post.title
    json.excerpt post.excerpt
    json.published_at post.published_at

Fragment caching for expensive JSON serialization

rails caching performance
by Alex Kumar 1 tab

Share this code

Here's the card — post it anywhere.

Building a Minimal useQuery Hook with a Shared Cache Provider in React — share card
Link copied