Skip to content

Queries (useQuery)

The flagship hook. Reads server state, caches it, deduplicates concurrent calls, retries, and refetches on focus / reconnect / stale.

Signature

queryAtom.useQuery<T>(options: AddQueryOptions<T>): Query<T>
type AddQueryOptions<T> = {
queryKey: QueryKey;
queryFn: (ctx: { signal: AbortSignal }) => Promise<T>;
onSuccess?: (data: T, query: Query<T>) => void;
onError?: (error: any, query: Query<T>) => void;
watch?: boolean; // default true
retry?: number; // default 0
retryDelay?: number | ((attempt: number) => number);
retryCondition?: (error: any, attempt: number) => boolean;
staleTime?: number; // default 0
gcTime?: number; // default 5 * 60_000
refetchOnMount?: boolean; // default true
refetchOnWindowFocus?: boolean; // default false
refetchOnReconnect?: boolean; // default false
};

The Query<T> shape

FieldTypeMeaning
dataT | undefinedundefined until the first success.
isLoadingbooleantrue only during the first fetch (no data yet). Use for skeleton loaders.
isFetchingbooleantrue during any fetch (initial OR background). Use for “refreshing…” indicators.
isErrorbooleanLast attempt failed (post-retries).
errorunknown | nullThe error object.
state"idle" | "loading" | "error" | "success"Coarse lifecycle.
isRetryingbooleanCurrently waiting between retry attempts.
lastModified, lastSuccessAt, lastErrorAtnumberTimestamps.
fetchCount, retryCount, maxRetriesnumberCounters.
queryKey, hashKey, queryFnStored on the entry.

Patterns

Basic

const { data, isLoading, error } = queryAtom.useQuery<User[]>({
queryKey: ["users"],
queryFn: ({ signal }) => fetch("/api/users", { signal }).then(r => r.json()),
staleTime: 60_000,
});

With dependent key

const userId = useParams().id;
const { data } = queryAtom.useQuery<User>({
queryKey: ["users", userId],
queryFn: ({ signal }) =>
fetch(`/api/users/${userId}`, { signal }).then(r => r.json()),
});

Changing userId swaps the cache entry. The previous entry stays in the cache (eligible for GC).

With retry

queryAtom.useQuery({
queryKey: ["users"],
queryFn,
retry: 3,
retryDelay: attempt => 1000 * 2 ** attempt, // 1s, 2s, 4s
retryCondition: (err, attempt) => {
return !(err instanceof TypeError); // don't retry on schema mismatch
},
});

Window focus / reconnect

queryAtom.useQuery({
queryKey: ["dashboard"],
queryFn,
staleTime: 30_000,
refetchOnWindowFocus: true, // refetch if stale when tab regains focus
refetchOnReconnect: true, // refetch when navigator.online flips back
});

Granular subscriptions

When a component only cares about one field:

const isLoading = queryAtom.useLoadChange(["users"]);
const data = queryAtom.useDataChange<User[]>(["users"]);
const error = queryAtom.useErrorChange(["users"]);
const fetching = queryAtom.useQueryChange(["users"], "isFetching");

Only re-renders when the named field changes — not when sibling fields on the same query change.

Snapshot without re-rendering — watch: false

queryAtom.useQuery({
queryKey: ["users", filterState],
queryFn,
watch: false, // one-shot snapshot; the query still loads in the background,
// the component just doesn't re-render on changes.
});

watch: false disables the per-render subscription only. The cache entry is still created and the initial fetch still runs — use queryAtom.getData(key) from event handlers if you want a true “fire and forget” read.

Key hashing

Keys are JSON-hashed with sorted object keys:

  • ["users", { role: "admin", active: true }] and ["users", { active: true, role: "admin" }] → same entry.
  • ["users", "1|2"] and ["users", 1, 2] → different entries.

Concurrent dedup

Three components mounting with the same queryKey result in one queryFn invocation. Subsequent components join the in-flight promise.

queryFn freshness

The hook stashes the latest queryFn closure in a registry keyed by hash. Refetches always use the freshest closure — even if the component has re-rendered with different captured props/state since the entry was created.

Abort behavior

  • The signal passed to queryFn is aborted when:
    • A newer fetch starts for the same key.
    • queryAtom.destroyQuery(key) is called.
  • The signal is NOT aborted on consumer unmount. Strict Mode and route bounces don’t kill in-flight fetches.