Skip to content

Feature Request: Official Recommended Global Request Cache Pattern in Svelte #16434

@mithi

Description

@mithi

Describe the problem

While building apps with Svelte 5, I’ve often run into the need to share and reuse the state of async requests (data, loading, error) across multiple components — similar to how React Query or SWR work in React.

As far as I know, there isn’t an official or idiomatic pattern in Svelte for implementing a global request cache. This often leads developers (myself included) to reinvent the wheel, usually borrowing patterns from React that don’t feel quite natural in Svelte.

I’m aware of TanStack Query for Svelte, but it seems to have several issues with Svelte 5 and doesn’t appear to be prioritized at the moment.

Below is my current approach. I suspect I’ve leaned too heavily on React-style patterns and that it is not idiomatic Svelte 5. I’m still very new to Svelte 5, so it’s possible I’m overcomplicating things — and I’d appreciate any guidance...

📐 Current Approach (Most likely wrong / an anti-pattern)

I maintain a global store:

type GlobalRequestCacheStore = SvelteMap<string, RequestCache<StableKey, unknown>>;
let globalRequestCacheStore: GlobalRequestCacheStore | null = null;

It should be initialized at the root of the app with:

const globalRequestCache = () => {
  if (globalRequestCacheStore != null) {
    throw new Error('globalRequestCacheStore already exists');
  }

  globalRequestCacheStore = new SvelteMap();
  return () => {
    globalRequestCacheStore = null;
  };
};

The root component calls like this:

const destroyCache = globalRequestCache();
onDestroy(() => destroyCache());

The store holds instances of class given a corresponding hash of the key.
(Below is a simplified version of the class implementation)

class RequestCache<K extends StableKey, T> {
  #state = $state<{ data: T | null; error: Error | null; status: RequestStatus }>({
    data: null, error: null, status: 'idle' });

  #requestFn: RequestFn<K, T>;
  #key: K;
  #hash: string;

  constructor(key: K, requestFn: RequestFn<K, T>) {
    this.#key = key;
    this.#requestFn = requestFn;
    this.#hash = stableHash(key);
    this.request();
  }

  mutate = (fn: (current: T | null) => T | null) => {
    this.#state.data = fn(this.#state.data);
  };

  request = (): Promise<T | null> => {
    this.#state = { data: null, error: null, status: 'loading' };

    return this.#requestFn(this.#key)
      .then((r) => {
        this.#state = { data: r, error: null, status: 'completed' };
        return r;
      })
      .catch((e) => {
        this.#state = { data: null, error: e, status: 'errored' };
        return null;
      });
  };

  get state() {
    return this.#state;
  }

  get hash() {
    return this.#hash;
  }
}

Components can access a cached request via:

const getRequestCache = <K extends StableKey, T>(
  key: K,
  requestFn: RequestFn<K, T>,
): RequestCache<K, T> => {
  const hash = stableHash(key);

  if (globalRequestCacheStore == null) {
    throw new Error('globalRequestCacheStore not initialized');
  }

  const existing = globalRequestCacheStore.get(hash);

  if (existing) {
    return existing as RequestCache<K, T>;
  }

  const instance = new RequestCache<K, T>(key, requestFn);
  globalRequestCacheStore.set(hash, instance);
  return instance;
};

Which you can wrap like:

export const getNote = (token: string, id: number) => {
  // getRequestCache(key, fetcher)
  return getRequestCache(
    { url: `http://api.com/note/${id}`, token },
    async ({ url, token }) => {
      await sleep(2000);
      return { url, token, note: 'I am a note!' };
    }
  );
};

And then in a child component:

const { id } = $props();
const requestInstance = getNote('my-token', id);
// requestInstance.state.status === "loading"

⚠️ The Problem

When id changes (via props), the getNote call is still referring to the cached instance for the old id, because the component doesn't call getNote again. You can wrap it with$derive because it creates an object and stores it ($derive must not include unsafe mutations or side effects)
My workaround for now is to force Svelte to recreate the child by wrapping it in a #key block:

{#key id}
  <Note id={id} />
{/key}

This works, but feels awkward — perhaps even an anti-pattern.

Describe the proposed solution

It would be nice the Svelte team provide an official, idiomatic recommendation for implementing a global request cache in Svelte, similar in spirit to how React Query or SWR work in React.

This would make it easy to:
Easily share and reuse async request state (data, loading, error) across multiple components.
Mutate and revalidate cached data from anywhere in the app, enabling patterns like optimistic updates, rollbacks on error, manual refresh actions, or keeping related views (e.g., a list of notes) in sync when items are created, updated, or deleted.
Ensure cached data remains consistent and reactive throughout the app, reflecting the latest state without manual intervention.

A recommended pattern, utility, or built-in API to support this common use case would be very helpful.

Importance

would make my life easier

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      pFad - Phonifier reborn

      Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

      Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy