-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Description
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