simple-svelte-query Svelte Themes

Simple Svelte Query

simple-svelte-query

Query/cache library for Svelte 5 inspired by TanStack Query, with one intentional architectural decision:

  • similarity: hierarchical keys, stale time, invalidation by key/prefix
  • difference: it caches the Promise itself, not only the resolved value

This reduces boilerplate in flows that use native Svelte await, while keeping async semantics explicit.

Installation

bun add simple-svelte-query
npm install simple-svelte-query
pnpm add simple-svelte-query
yarn add simple-svelte-query

Requirements

  • Svelte 5
  • For direct await expressions in <script>/markup, enable compilerOptions.experimental.async = true
  • Without async mode, consume queries with {#await query} instead

Quick start

import { 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>

API

Detailed reference: docs/API.md.

Public exports

  • QueryClient
  • Query
  • queryOptions

new Query(options)

  • options.queryKey: hierarchical key used by cache
  • options.queryFn: async fetcher receiving { signal, queryKey }
  • options.staleTime?: optional stale window in milliseconds
  • options.hashKey?: optional key hash function
  • instance API: key (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 config
  • options.persist.persister: required when persist exists; must implement get, set, del, clear
  • options.persist.hydrate?: (value) => Promise<value> transformation before using persisted values
  • options.persist.dehydrate?: (queryKey, value) => persistedValue | undefined; when it returns undefined, nothing is saved
  • expected shape:
new QueryClient({
    persist: {
        persister,
        hydrate,
        dehydrate
    }
});
  • staleTime and hashKey are stored as static defaults used by new Query instances

createQuery(() => options)

  • creates a reactive query
  • returns a PromiseLike<T> object with queryKey and pending
  • uses hydratable internally for SSR/hydration
  • works with {#await query} everywhere, or with direct await query when Svelte async mode is enabled

fetchQuery(options)

  • fetches and populates cache
  • respects staleTime (refetch only when stale)

ensureQuery(options)

  • ensures a cache entry without checking staleness

setQuery(queryKey, value)

  • injects a value or Promise into cache, wrapped with Promise.resolve(value)

removeQuery(query)

  • removes one cache entry using a Query instance
  • useful when you already have a constructed query object

getQuery(queryKey)

  • returns the cached Promise (or undefined)

invalidateQuery(queryKey)

  • exact invalidation by key

invalidateQueries(prefix?)

  • prefix-based invalidation; with no args invalidates everything
  • if you customize hashKey, prefix invalidation only works when the hash preserves key prefixes

clear()

  • clears all cached queries

queryOptions(options)

  • identity helper to preserve queryKey literal types and propagate them to queryFn

Code examples

createQuery (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();

Invalidation by exact key and by prefix

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>

Similarities and differences vs TanStack Query

Similarities

  • hierarchical queryKey model
  • precise and batch invalidation
  • staleTime
  • cancellation support via AbortSignal

Intentional differences

  • no useQuery, no large state object (isFetching, status, etc)
  • direct consumption through await in component/script
  • promise-first cache, useful for async composition and predictable concurrent behavior

Scripts

bun run dev
bun run check
bun run lint
bun run test
bun run build

Publishing

bun run prepack
npm publish

License

MIT

Top categories

Loading Svelte Themes