> ## 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.

# How it works

> Understand how ConvexQueryClient bridges Convex WebSocket subscriptions and the TanStack Query cache.

## 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:

```ts theme={null}
["convexQuery", FunctionReference<"query">, args]
```

For example:

```ts theme={null}
["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:

```ts theme={null}
`convexQuery|${getFunctionName(queryKey[1])}|${JSON.stringify(convexToJson(queryKey[2]))}`
```

This is why `queryKeyHashFn` must be set globally when configuring the `QueryClient`:

```ts theme={null}
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      queryKeyHashFn: convexQueryClient.hashFn(),
      queryFn: convexQueryClient.queryFn(),
    },
  },
});
```

<Warning>
  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.
</Warning>

## Cache update flow

<Steps>
  <Step title="Component mounts">
    A component calls `useQuery(convexQuery(api.foo.bar, args))`. TanStack Query
    checks the cache for a matching key hash.
  </Step>

  <Step title="Query added to cache">
    If no entry exists, TanStack Query fires an `"added"` event on the
    `QueryCache`.
  </Step>

  <Step title="Convex subscription created">
    `ConvexQueryClient` receives the `"added"` event and calls
    `convexClient.watchQuery(api.foo.bar, args)`, opening a WebSocket
    subscription to the Convex backend.
  </Step>

  <Step title="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`.
  </Step>

  <Step title="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.
  </Step>

  <Step title="Cache updated">
    `ConvexQueryClient` calls `queryClient.setQueryData(queryKey, newValue)` to
    write the latest value into the TanStack Query cache.
  </Step>

  <Step title="React re-renders">
    TanStack Query notifies all observers of the updated cache entry. Every
    component subscribed via `useQuery` re-renders with the new data.
  </Step>
</Steps>

## 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.

<Tip>
  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.
</Tip>

## Server vs. client behavior

`ConvexQueryClient` detects the environment with `typeof window === "undefined"` and adjusts accordingly.

| Environment  | `queryFn` 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. |
| Client       | Uses the `ConvexReactClient` WebSocket connection for both the initial fetch and all ongoing subscriptions.                                                                                            |

<Note>
  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).
</Note>

## Error handling

When a Convex query function throws, `ConvexQueryClient` catches the error from `watch.localQueryResult()` and updates the query's state directly:

```ts theme={null}
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.
