Data cache API — useQuery, mutation, QueryClient#

The client cache from janux/query (re-exported by janux/client). Server state lives here; component state stays in schema. Guide: Data cache & URL state.

import { useQuery, mutation, QueryClient, getQueryClient, hashKey } from 'janux/query';

useQuery(bag, id, getOptions, client?)#

The one you'll use. Inside a component, binds a reactive read to the cache, memoized per bag+id so re-renders reuse the same handle.

const products = useQuery(bag, 'catalog', () => ({
  queryKey: ['catalog', state.tag],
  queryFn: () => fetchCatalog(state.tag),
  staleTime: 30_000,
}));

products.data.value;        // T | undefined
products.isPending.value;   // no data yet
products.isFetching.value;  // a request is in flight (including refetches)
products.error.value;
await products.refetch();

getOptions is a getter: reading signals inside it is how the key becomes reactive. When the key changes, the handle switches to the new cache entry inside one stable effect — the old subscription is disposed, the new one fetched if stale. That's why a derived wrapping a useQuery has no dependencies and doesn't thrash.

QueryOptions: queryKey (any serializable array), queryFn, staleTime (ms before a read refetches), gcTime (ms an unobserved entry survives).

query(options, client?)#

The primitive under useQuery, without the per-bag memoization. Use it outside a component — in a helper or a foreign bridge — where you own disposal via the ownership scope. Same QueryHandle shape; options may be an object or a getter.

mutation(options, client?)#

const addItem = mutation({
  mutationFn: (vars: { id: string }) => post('/api/cart', vars),
  onMutate: (vars) => optimisticallyAdd(vars),        // returns ctx
  onError: (error, vars, ctx) => rollback(ctx),
  onSuccess: (data, vars, ctx) => toast('Added'),
  onSettled: () => getQueryClient().invalidateQueries(['cart']),
});

await addItem.mutate({ id: 'sku-1' });
addItem.isPending.value;

The four callbacks run in that order; whatever onMutate returns is handed to onError/onSuccess as ctx, which is the optimistic-update pattern with rollback.

QueryClient#

One cache instance. The client runtime creates it; SSR gets a fresh one per request, so server rendering never leaks data between users.

Method What it does
getQuery(options) The cache entry for options.queryKey, created if absent
getQueryData(key) Current data for a key, or undefined
setQueryData(key, data) Writes data directly (optimistic updates)
invalidateQueries(key?) Refetches everything matching the key prefix — no key means all
mutate(options, vars) Runs a mutation's lifecycle (what mutation() wraps)
dehydrate() Serializable snapshot of every entry — for SSR handoff
hydrate(entries) Restores a snapshot on the client

invalidateQueries(['cart']) matches by prefix, so it also refreshes ['cart', 'summary']. Failed refetches are swallowed on purpose: invalidation must not reject.

getQueryClient()#

Returns the ambient client, creating it on first use. useQuery prefers bag.ctx.queryClient when present (that's how the per-request SSR client is threaded), then falls back to this.

hashKey(key)#

Turns a QueryKey into the cache's string hash. Object keys are sorted, so ['p', { a: 1, b: 2 }] and ['p', { b: 2, a: 1 }] are the same entry — you don't have to normalize your keys by hand.

hashKey(['catalog', { tag: 'display', page: 1 }]);

Related: Data cache & URL state · signal · Ownership