@shadow-js/query
v1.0.4
Published
Fetch, cache and update server data with queries, mutations and optimistic updates
Readme
@shadow-js/query
Server state for ShadowJS 1.0.0: a bounded cache of server data keyed by plain-data arrays, with
useQuery, useMutation, useInfiniteQuery, useQueries and useMutationState. Components that ask for
the same key share one request. Data is refreshed on mount, focus, reconnect and an interval; writes can
update the screen at once and roll back. The cache can be kept in a storage between visits, writes made offline
wait in a queue and replay in order, and the server can send the cache to the browser, streamed as it fills. It is
built only on the public API of @shadow-js/core.
Install
npm install @shadow-js/queryRequires @shadow-js/core (a peer dependency).
Quick start
import { createQueryClient } from "@shadow-js/query";
export const client = createQueryClient({
defaultOptions: { queries: { staleTime: 30_000 } },
});import { useQuery } from "@shadow-js/query";
import { client } from "./client";
// A stand-in backend: replace with fetch().
const loadTodos = (): Promise<string[]> => Promise.resolve(["read the docs"]);
export function useTodos() {
return useQuery(["todos"], () => loadTodos(), { client });
}import { flush, renderHook } from "@shadow-js/testing";
import { describe, expect, it } from "vitest";
import { client } from "./client";
import { useTodos } from "./todos";
describe("useTodos", () => {
it("fetches once and shares the data", async () => {
const view = renderHook(() => useTodos());
expect(view.result.current.status()).toBe("pending");
await flush();
expect(view.result.current.data()).toEqual(["read the docs"]);
expect(client.queryCache.find({ key: ["todos"] })[0]?.observers).toBe(1);
view.dispose();
});
});API
Client
createQueryClient(options?): a client with its own cache.defaultOptions.queriessetsstaleTime(default 0),gcTime(300 000 ms),retry(3),retryDelay,refetchOnWindowFocus,refetchOnReconnect,refetchOnMount,structuralSharingandnetworkMode(online: wait while offline,always: never wait; any value other thanonlineneeds theofflinefeature).queryCache.maxQueries(500) andmutationCache.maxMutations(100) bound the caches.client.getCache(key)/client.setCache(key, value | updater, { updatedAt? }): read and write data.client.invalidate(filter?, { refetchType? }): mark stale and refetch the mounted matches.client.refetchQueries(filter?),client.prefetch(key, fn, options?)(never rejects),client.fetchQuery(key, fn, options?)(rejects),client.cancel(filter?),client.reset(),client.isFetching(filter?).client.queryCacheandclient.mutationCache: read-only views (size,find).queryCache.find(filter?)returns snapshots withqueryKey,queryHash,status,fetchStatus,data,error,dataUpdatedAt,isInvalidatedandobservers;queryCache.subscribe(listener)reports everyadded,updatedandremovedchange and returns the unsubscribe function (it needs theinspectionfeature).client.dehydrate(options?)andclient.hydrate(state): the state for the browser (they need thehydrationfeature).client.resumePausedMutations(): replays the mutations that wait in the offline queue now and resolves when it has drained (needs thequeuefeature).mutationCache.find()marks a waiting callisPaused: true.QueryClientContext,useQueryClient()andrunWithQueryClient(client, fn): provide a client without passing it to each hook.exponentialBackoff({ base, max, jitter, random }): aretryDelayfunction (doubling, capped, optional full jitter with an injectable random source).hashKey(key): the hash of a key, the same one the loader cache uses.
Hooks
useQuery(key, queryFn, options?):data(),error(),status(),fetchStatus(),isFetching(),isStale(),isPending(),isSuccess(),isError(),isPlaceholderData()andrefetch(). Options:enabled,staleTime,gcTime,retry,retryDelay,refetchOnWindowFocus,refetchOnReconnect,refetchOnMount,structuralSharing,refetchInterval,select,placeholderData,keepPreviousData,initialData,initialDataUpdatedAt,networkMode,suspense,throwOnError,client.useMutation(mutationFn, options?):mutate,mutateAsync,data(),error(),status(),isPending(),variables(),reset(). Options:mutationKey,scope({ id }: calls with the same id run one at a time, in order; needs thescopefeature),retry(default 0),retryDelay,client. Callbacks:onMutate,onSuccess,onError,onSettled.networkMode(online,always) decides whether a call made offline waits in the queue (needs thequeuefeature).useMutationState({ mutationKey?, status?, client? }): an accessor of the matching mutation records.useInfiniteQuery(key, queryFn, options): the query result plusfetchNextPage(),fetchPreviousPage(),hasNextPage(),hasPreviousPage(),isFetchingNextPage()andisFetchingPreviousPage(). Options:initialPageParam,getNextPageParam,getPreviousPageParam,maxPages, and theuseQueriesdefinition options below.useQueries(definitions): one result per definition, typed in order. A definition takeskey,queryFnand theuseQueryoptions that do not refer to the data type (noselect,placeholderData,keepPreviousDataorinitialData).
Optional features
createQueryClient({ features: [...] }) takes the optional capabilities of a client. Each is a subpath, so a
client bundles only the ones it lists. A member that needs a feature the client lacks throws a TypeError that
names the feature and its import.
offline(@shadow-js/query/offline):networkModeother thanonline, and the wait of a request while offline.scope(@shadow-js/query/scope): thescopeoption ofuseMutation.inspection(@shadow-js/query/inspection):queryCache.subscribe.queryCache.findneeds nothing.hydration(@shadow-js/query/hydration):client.dehydrate()andclient.hydrate().queue(@shadow-js/query/queue): a retry waits for the connection, a mutation made offline waits in a queue that replays one call at a time in call order, andclient.resumePausedMutations().persist(@shadow-js/query/persist):persist(options)makes a feature that keeps the cache in a storage (see below).full(@shadow-js/query/full): every export of the package and theoffline,scope,inspectionandhydrationfeatures in one list (features: full).queueandpersistare listed on their own.
@shadow-js/query/persist
persist({ storage, key?, buster?, maxAge?, throttleMs?, shouldPersist?, maxEntryBytes?, maxBytes?, mutations?, broadcast?, onError? }): a feature that writes the successful queries tostorage(the three calls oflocalStorage, each may return a promise) and reads them back when the client is created. It returns the feature withrestored(a promise that settles once the stored cache is in),flush(),clear()anddispose().busterdrops stored data of another version;maxAge(24 h) skips old entries;throttleMs(1 s) spaces writes, which also happen when the page is hidden or closed;maxEntryBytes(256 KiB) andmaxBytes(2 MiB) bound the store, dropping the oldest entries first;shouldPersist(query)narrows what is kept. Errors, pending queries and mutations are never written by default.- Storage problems (a full or failing storage, data JSON cannot hold, a corrupt or tampered store) go to
onError(error)witherror.kindofrestore,write,quota,serializeorsize; they never throw into your code. WhereBroadcastChannelexists, tabs read each other's writes. mutations: [{ mutationKey, mutationFn, ... }](withqueuelisted beforepersist) keeps calls that wait offline and replays them after a reload, at least once, in call order.
@shadow-js/query/server
dehydrateScript(state, id?): the state as an escaped<script type="application/json">element.hydrateFromDocument(client, id?): seeds the client from that element in the browser.createDehydrationStream(client, options?): for a streamed response;next()renders the queries that finished since the last call as one script element, or an empty string.hydrateFromStream(client, options?): reads those elements in the browser as the parser adds them; returns a function that stops watching.
Limits
- Nothing is fetched on the server; prefetch in the request handler and dehydrate.
- Keys and dehydrated data are plain JSON data (see the contract).
- A query whose last observer leaves is aborted and its late answer is dropped.
- While the browser is offline a request waits (
fetchStatus"paused") and is sent on theonlineevent, unlessnetworkModeis"always". A promise that waits settles when its request runs or is canceled. Both need theofflinefeature: without it a query runs and fails likefetch. - The
queuefeature holds retries and mutations while offline. The queue replays one call at a time; a call that never settles holds it. It is in memory unlesspersiststores it, where delivery is at least once (use an idempotency key) and a replayed call does not runonMutate. persiststores JSON only (aDate,Mapor class instance is skipped and reported) in a storage with a limit; it does not encrypt, so filter private data withshouldPersist. A failed write is retried at the next change.- A time stamped ahead of this clock in a hydrated or restored entry counts as now.
setCachewithundefinedwrites nothing, so a rollback cannot return to "no data"; start optimistic updates from the data you hold.- Failed fetches retry with backoff, then report their error.
Production error codes
A production build keeps a short code instead of the development explanation, so message text never reaches user bundles. Search this table for the code. In the explanation, … stands for a value the development message names. A code followed by a word names the option at fault.
| Code | Development explanation |
| --- | --- |
| QUERY_LIMIT | createQueryClient: … must be a positive integer, got …. |
Links
- ShadowJS documentation: guides and the API reference.
Dependencies
- Runtime: none.
- Peers:
@shadow-js/core^1.0.3: reactive state for the query hooks.
Types
- Names from the root:
QueryKey,QueryDefinition,QueryResults,QueryClientContext,useQuery,useQueries,useInfiniteQuery. - Type tests in
test-types/are compile-time checks;npm run typecheck:typesruns them.
Support
- Node.js
>=22.12.0. ESM only: useimport;requireis not supported. - Browsers: current evergreen releases.
- Versioning: every
@shadow-js/*package releases together at one version. Upgrade them as one set. - Documentation: ShadowJS documentation.
Using, changing and contributing
- Use the package (a person or a coding agent that builds with it): this README, and SKILL.md, the short page an agent reads first.
- Change the package (fix a bug, add a feature): AGENTS.md says how the repository is laid out, how to implement and test a change, and how a pull request is reviewed. It is written for people and coding agents alike.
- Take part: CONTRIBUTING.md is the short guide; SECURITY.md is for vulnerabilities.
