Query/cache library for Svelte 5 inspired by TanStack Query, with one intentional architectural decision:
Promise itself, not only the resolved valueThis reduces boilerplate in flows that use native Svelte await, while keeping async semantics explicit.
bun add simple-svelte-query
npm install simple-svelte-query
pnpm add simple-svelte-query
yarn add simple-svelte-query
await expressions in <script>/markup, enable compilerOptions.experimental.async = true{#await query} insteadimport { QueryClient } from 'simple-svelte-query';
const queryClient = new QueryClient();
<script lang="ts">
import { QueryClient } from 'simple-svelte-query';
const queryClient = new QueryClient();
let search = $state('phone');
const productsQuery = queryClient.createQuery(() => ({
queryKey: ['products', search],
queryFn: ({ signal }) =>
fetch(`/api/products?q=${encodeURIComponent(search)}`, { signal }).then((r) => r.json())
}));
</script>
<ul style:opacity={productsQuery.pending ? 0.4 : 1}>
{#each (await productsQuery).items as item}
<li>{item.name}</li>
{/each}
</ul>
Detailed reference: docs/API.md.
QueryClientQueryqueryOptionsnew Query(options)options.queryKey: hierarchical key used by cacheoptions.queryFn: async fetcher receiving { signal, queryKey }options.staleTime?: optional stale window in millisecondsoptions.hashKey?: optional key hash functionkey (hashed key), isStale(lastUpdated), fetch(queryKey, signal?)new QueryClient(options?)options.staleTime?: defines the default staleTime (ms)options.hashKey?: function to hash queryKey into internal string key (default JSON.stringify)options.persist?: optional persistence configoptions.persist.persister: required when persist exists; must implement get, set, del, clearoptions.persist.hydrate?: (value) => Promise<value> transformation before using persisted valuesoptions.persist.dehydrate?: (queryKey, value) => persistedValue | undefined; when it returns undefined, nothing is savednew QueryClient({
persist: {
persister,
hydrate,
dehydrate
}
});
staleTime and hashKey are stored as static defaults used by new Query instancescreateQuery(() => options)PromiseLike<T> object with queryKey and pendinghydratable internally for SSR/hydration{#await query} everywhere, or with direct await query when Svelte async mode is enabledfetchQuery(options)staleTime (refetch only when stale)ensureQuery(options)setQuery(queryKey, value)Promise into cache, wrapped with Promise.resolve(value)removeQuery(query)Query instancegetQuery(queryKey)Promise (or undefined)invalidateQuery(queryKey)invalidateQueries(prefix?)hashKey, prefix invalidation only works when the hash preserves key prefixesclear()queryOptions(options)queryKey literal types and propagate them to queryFncreateQuery (reactive)const productsQuery = queryClient.createQuery(() => ({
queryKey: ['products', filters],
queryFn: ({ signal }) => fetchProducts(filters, signal)
}));
// useful for dimming stale data while the next key settles
const style = productsQuery.pending ? 0.4 : 1;
const products = await productsQuery;
queryOptions (typed options helper)const userQueryOptions = queryOptions({
queryKey: ['user', userId] as const,
queryFn: ({ queryKey: [, id], signal }) =>
fetch(`/api/users/${id}`, { signal }).then((r) => r.json())
});
const user = await queryClient.fetchQuery(userQueryOptions);
fetchQuery (imperative)const user = await queryClient.fetchQuery({
queryKey: ['user', userId],
queryFn: ({ signal }) => fetch(`/api/users/${userId}`, { signal }).then((r) => r.json())
});
ensureQuery (does not revalidate staleness)const postsPromise = queryClient.ensureQuery({
queryKey: ['posts', userId],
queryFn: ({ signal }) => fetch(`/api/posts?user=${userId}`, { signal }).then((r) => r.json())
});
const posts = await postsPromise;
setQuery + getQuery (seed/optimistic cache)queryClient.setQuery(['user', userId], { id: userId, name: 'Optimistic name' });
queryClient.setQuery(['user', userId], Promise.resolve({ id: userId, name: 'From promise' }));
const cachedUser = await queryClient.getQuery<{ id: string; name: string }>(['user', userId]);
clear (drop all cached queries)queryClient.clear();
queryClient.invalidateQuery(['user', userId]); // exact
queryClient.invalidateQueries(['users']); // all keys starting with ['users', ...]
queryClient.invalidateQueries(); // all
createQuery with reactive key switching<script lang="ts">
import { QueryClient } from 'simple-svelte-query';
const queryClient = new QueryClient();
let category = $state('smartphones');
const productsQuery = queryClient.createQuery(() => ({
queryKey: ['products', category],
queryFn: ({ signal }) =>
fetch(`/api/products?category=${category}`, { signal }).then((r) => r.json())
}));
</script>
<button onclick={() => (category = 'laptops')}>Switch category</button>
<div style:opacity={productsQuery.pending ? 0.4 : 1}>
{#each (await productsQuery).items as item}
<div>{item.name}</div>
{/each}
</div>
queryKey modelstaleTimeAbortSignaluseQuery, no large state object (isFetching, status, etc)await in component/scriptbun run dev
bun run check
bun run lint
bun run test
bun run build
bun run prepack
npm publish
MIT