@reckona/mreact-query
v0.0.194
Published
Server state and async cache primitives for mreact.
Maintainers
Readme
@reckona/mreact-query
@reckona/mreact-query provides server state and async cache primitives for mreact. Loaders can prefetch data on the server, while client components hydrate and continue using the same query cache.
Basic Usage
import {
createQuery,
createQueryClient,
dehydrate,
getQueryClient,
hydrate,
syncQueryClientAcrossTabs,
} from "@reckona/mreact-query";
const queryClient = createQueryClient();
await queryClient.prefetchQuery({
queryKey: ["profile"],
retry: 2,
retryDelay: 100,
queryFn: ({ signal }) => fetch("/api/profile", { signal }).then((res) => res.json()),
});
const state = dehydrate(queryClient);
hydrate(getQueryClient(), state);Core APIs
createQueryClient()creates a query cache.fetchQuery()andprefetchQuery()execute async data functions and cache their results.- Query functions receive
{ queryKey, signal }; passsignaltofetch()socancelQueries()can abort in-flight work. retryandretryDelayopt into bounded retries for transient failures.- Query results expose
errorReasonas"aborted","retry-exhausted","network", or"unknown"so UI can avoid treating cancellations like user-visible failures. cancelQueries()aborts in-flight queries by key prefix without retrying the canceled request.removeQueries()aborts matching in-flight queries, evicts matching cache entries, and resets subscribed observers to an empty pending result.createQuery()creates a reactive query observer. It auto-fetches empty queries in browsers by default and remains observe-only during server render. Hydrated entries render immediately, then revalidate on mount unless their serverupdatedAttimestamp is still covered bystaleTime; passautoFetch: falseto require loader-prefetched data only.createQuery()acceptsgcTimeto evict an idle cache entry after the last observer disposes. It is disabled by default; pass a non-negative millisecond value when short-lived browser views should release data after unmount.createQuery()can opt into browser revalidation withrefetchOnWindowFocus,refetchOnReconnect, andrefetchOnInvalidate. These hooks are disabled by default and refetch through the same cache entry and abort signal path as manualrefetch().createQuery()andcreateInfiniteQuery()acceptrefetchIntervalin milliseconds for browser polling. Each interval is scheduled after the preceding fetch settles, pauses while the document is hidden, and is cleared automatically when the observer disposes.createInfiniteQuery()stores cursor pages under one query key, exposespages,pageParams,hasNextPage, andfetchNextPage(), and dedupes concurrent requests for the same next page.createMutation()handles mutations and invalidation.queryClient.setQueryData(queryKey, updater)can derive a new cached value from the previous value without fetching.- Mutation lifecycle hooks run in this order:
onMutate,mutationFn, state update,onSuccess, query invalidation, thenonSettled. On failure, state updates beforeonErrorandonSettled. The value returned byonMutateis passed toonErrorandonSettled, which supports optimistic rollback without external bookkeeping. dehydrate()andhydrate()move query state from server to client while preserving each successful query'supdatedAttimestamp forstaleTimechecks.getQueryClient()returns the browser singleton query client.syncQueryClientAcrossTabs()optionally coordinates same-origin browser tabs withBroadcastChanneland Web Locks. Query messages require a non-default scoped channel and anincludeQueryallowlist; successful query data is shared only whenbroadcastQueryDataorsingleFlightis explicitly enabled within that scope.
Router Usage
Use the request-scoped query client inside loader, then hydrate the browser singleton returned by getQueryClient(). This keeps large apps centered around query keys instead of passing every server-state value through page props. Server integrations that call runWithQueryClient() directly must first install AsyncLocalStorage with installQueryAsyncStorage(); without a request scope storage, runWithQueryClient() throws instead of using module-level state.
Infinite Queries
Use createInfiniteQuery() for cursor timelines and feeds that should not hand-roll request dedupe or page state in components.
const feed = createInfiniteQuery(queryClient, {
queryKey: ["timeline"],
initialPageParam: null as string | null,
queryFn: ({ pageParam, signal }) =>
fetch(`/api/timeline?cursor=${pageParam ?? ""}`, { signal }).then((res) => res.json()),
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
await feed.fetchNextPage();
feed.result.get().pages.flatMap((page) => page.items);Set refetchOnWindowFocus or refetchOnReconnect when a browser observer should refresh on visibility/focus or network reconnect. Dispose observers when a component unmounts so browser listeners are removed.
Cross-Tab Sync
Use syncQueryClientAcrossTabs() when multiple same-origin tabs should observe each other's invalidations or avoid duplicate focus/reconnect refetches. The function mutates the provided client and returns a disposer that restores the original methods.
const queryClient = getQueryClient();
const disposeQuerySync = syncQueryClientAcrossTabs(queryClient, {
channel: `mreact-query:v1:user:${sessionId}`,
includeQuery: (queryKey) => queryKey[0] === "dashboard",
singleFlight: true,
});The adapter only sends or receives query messages when channel is a non-default name scoped to the current authenticated user, tenant, or other namespace and includeQuery explicitly allows the query key. Channel names are not secrets or authorization boundaries: any trusted or untrusted script running in the same origin can open the same BroadcastChannel, observe shared successful data, or send messages for allowed keys. Use data sharing only when the whole same-origin runtime is trusted, and do not share tokens, authorization-bearing data, sensitive PII, or query keys that reveal sensitive information. invalidateQueries({ queryKey }) and removeQueries({ queryKey }) are broadcast inside that namespace by default; keyless invalidations and removals stay local. Set broadcastQueryData: true only for query keys whose successful data is safe to share inside the channel. singleFlight: true uses Web Locks when available and hands the leader's successful fetch result to follower tabs so only one tab needs to perform a same-key fetch; without Web Locks or the required scope options, singleFlight falls back to local fetch behavior without leader election or implicit result handoff.
