Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/get-convex/convex-react-query/llms.txt

Use this file to discover all available pages before exploring further.

Overview

ConvexQueryClient acts as a bridge between the Convex backend and TanStack Query’s QueryCache. It listens to cache events and manages WebSocket subscriptions so that your components always receive fresh data without polling. The core flow is:
  • ConvexQueryClient subscribes to the TanStack QueryCache via queryCache.subscribe().
  • When a query with a "convexQuery" key is added to the cache (the first observer mounts), it calls convexClient.watchQuery() to open a subscription to the Convex backend over WebSocket.
  • When the Convex server pushes a new result, ConvexQueryClient calls queryClient.setQueryData() to update the cache, which triggers React re-renders.
  • When the query is removed from the cache (after gcTime elapses with no observers), ConvexQueryClient calls unsubscribe() on the underlying Watch to close the Convex subscription.

Query key structure

Every Convex query is identified by a 3-element tuple query key:
["convexQuery", FunctionReference<"query">, args]
For example:
["convexQuery", api.messages.list, {}]
["convexQuery", api.messages.search, { query: "hello", limit: 5 }]
TanStack Query’s default hashKey serializes query keys with JSON.stringify, which cannot correctly serialize Convex FunctionReference objects (they carry metadata like a function name string, not a plain value). convexQueryClient.hashFn() handles this by serializing the key as:
`convexQuery|${getFunctionName(queryKey[1])}|${JSON.stringify(convexToJson(queryKey[2]))}`
This is why queryKeyHashFn must be set globally when configuring the QueryClient:
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      queryKeyHashFn: convexQueryClient.hashFn(),
      queryFn: convexQueryClient.queryFn(),
    },
  },
});
If you forget to set queryKeyHashFn globally, different components subscribing to the same Convex query may create duplicate cache entries, leading to redundant subscriptions and stale renders.

Cache update flow

1

Component mounts

A component calls useQuery(convexQuery(api.foo.bar, args)). TanStack Query checks the cache for a matching key hash.
2

Query added to cache

If no entry exists, TanStack Query fires an "added" event on the QueryCache.
3

Convex subscription created

ConvexQueryClient receives the "added" event and calls convexClient.watchQuery(api.foo.bar, args), opening a WebSocket subscription to the Convex backend.
4

Initial fetch

ConvexQueryClient.queryFn() is called by TanStack Query to populate the cache with the first value. On the client this runs a WebSocket query; on the server it makes an HTTP request via ConvexHttpClient.
5

Server pushes result

When the Convex server has a new result (on mount or after a data change), the onUpdate callback registered with watch.onUpdate() fires.
6

Cache updated

ConvexQueryClient calls queryClient.setQueryData(queryKey, newValue) to write the latest value into the TanStack Query cache.
7

React re-renders

TanStack Query notifies all observers of the updated cache entry. Every component subscribed via useQuery re-renders with the new data.

Subscription lifecycle

TanStack Query separates the concept of “observers” (mounted useQuery hooks) from the cache entry itself. ConvexQueryClient uses this distinction to manage subscription lifetime:
  • While at least one useQuery observer is mounted, the subscription is active and the cache entry is kept alive.
  • When the last observer unmounts ("observerRemoved" event with zero remaining observers), TanStack Query starts the gcTime countdown. The subscription remains active and the cached value is preserved during this window.
  • Once gcTime expires, TanStack Query fires a "removed" event. ConvexQueryClient then calls unsubscribe() on the Watch object, closing the Convex WebSocket subscription.
During the gcTime window, remounting the same query is instant: the cached value is already available and the subscription is still live. Set gcTime to at least a few seconds (e.g., 10_000) for components that mount and unmount frequently.

Server vs. client behavior

ConvexQueryClient detects the environment with typeof window === "undefined" and adjusts accordingly.
EnvironmentqueryFn behavior
Server (SSR)Uses ConvexHttpClient to make HTTP requests. No WebSocket is created. By default, uses consistentQuery so that multiple queries in the same render return results from the same logical timestamp.
ClientUses the ConvexReactClient WebSocket connection for both the initial fetch and all ongoing subscriptions.
You can opt out of consistent SSR queries by setting dangerouslyUseInconsistentQueriesDuringSSR: true in ConvexQueryClientOptions. This trades data consistency across queries for a faster SSR render (one HTTP roundtrip instead of two).

Error handling

When a Convex query function throws, ConvexQueryClient catches the error from watch.localQueryResult() and updates the query’s state directly:
query.setState({
  error: error as Error,
  status: "error",
  fetchStatus: "idle",
  // ...
});
The error surfaces through the standard TanStack Query API — the error field returned by useQuery — with no additional configuration required.

Build docs developers (and LLMs) love