# Advanced React Query Patterns

Prefetching, hydration, cache strategies, infinite queries, and escape hatches for complex scenarios.

## Prefetching and hydration

For the best user experience, prefetch data in Server Components and pass it to the client via `HydrationBoundary`. The client component receives cached data instantly with no loading state.

The proxy caller in `lib/query.ts` cannot run on the server: there is no page there to resolve its relative `proxyPath` against. The server has the API definition, so it prefetches with a direct caller built from it. Name the definition after the proxy's source — `defineApi({ name: "api", ... })` for the routes of the source registered as `api` — so that both callers build the same keys:

```ts title="lib/query.server.ts"
import { createQueryCaller } from "@noverax/courier-react"
import { api } from "@/api"

export const qServer = createQueryCaller(api)
```

```ts title="app/users/page.tsx"
// app/users/page.tsx — React Server Component
import { dehydrate, HydrationBoundary, QueryClient } from "@noverax/courier-react"
import { qServer } from "@/lib/query.server"
import { UserList } from "./user-list"

export default async function UsersPage() {
  const queryClient = new QueryClient()

  // Prefetch using the built-in prefetchQuery
  await qServer.users.prefetchQuery(queryClient, "getAll")

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <UserList />
    </HydrationBoundary>
  )
}
```

```ts title="app/users/user-list.tsx"
// app/users/user-list.tsx
"use client"

import { q } from "@/lib/query"

export function UserList() {
  // This picks up the prefetched data instantly — no loading state
  const { data } = q.users.useQuery("getAll")

  return (
    <ul>
      {data?.data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}
```

> **Query key must match**
>
> `prefetchQuery` automatically generates the same hierarchical key as `useQuery`. For `q.users.useQuery("getAll")`, the key is `["api", "users", "getAll"]`. With variables, include them as the fourth element. The first element is the source name: the proxy caller's first argument, and the direct caller's `defineApi({ name })` — derived from the `baseURL` host when there is none, which is why the definition should be named.

## Prefetching with variables

```ts title="app/users/[id]/page.tsx"
import { dehydrate, HydrationBoundary, QueryClient } from "@noverax/courier-react"
import { qServer } from "@/lib/query.server"
import { UserProfile } from "./user-profile"

export default async function UserPage({ params }: { params: { id: string } }) {
  const queryClient = new QueryClient()

  await qServer.users.prefetchQuery(queryClient, "getById", {
    variables: { id: Number(params.id) },
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <UserProfile userId={Number(params.id)} />
    </HydrationBoundary>
  )
}
```

## Cache invalidation patterns

React Query uses partial key matching for invalidation. Hierarchical keys enable invalidation at different levels:

```ts
import { useQueryClient } from "@noverax/courier-react"

const queryClient = useQueryClient()

// Invalidate a specific query with exact variables
queryClient.invalidateQueries({
  queryKey: ["api", "users", "getById", { id: 5 }],
})

// Invalidate all queries for a specific method (any variables)
queryClient.invalidateQueries({
  queryKey: ["api", "users", "getAll"],
})

// Invalidate all queries for a group
queryClient.invalidateQueries({
  queryKey: ["api", "users"],
})

// Invalidate every query for a source
queryClient.invalidateQueries({
  queryKey: ["api"],
})

// Invalidate everything
queryClient.invalidateQueries()
```

## Dependent queries

When one query depends on another's result, use the `enabled` option to create a chain. The second query only fires after the first succeeds:

```ts title="components/user-posts.tsx"
"use client"

import { q } from "@/lib/query"

export function UserPosts({ userId }: { userId: number }) {
  // First query: fetch the user
  const { data: user } = q.users.useQuery("getById", {
    variables: { id: userId },
  })

  // Second query: only runs after the first succeeds
  const { data: posts, isLoading } = q.posts.useQuery("getByAuthor", {
    variables: { authorId: user?.id! },
    enabled: !!user?.id,
  })

  if (isLoading) return <p>Loading posts...</p>

  return (
    <div>
      <h2>Posts by {user?.name}</h2>
      <ul>
        {posts?.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  )
}
```

## Parallel queries

Multiple `useQuery` calls in the same component run in parallel automatically. React Query manages all the loading and error states independently:

```ts title="components/dashboard.tsx"
"use client"

import { q } from "@/lib/query"

export function Dashboard() {
  // These queries run in parallel automatically
  const { data: users, isLoading: usersLoading } = q.users.useQuery("getAll")
  const { data: posts, isLoading: postsLoading } = q.posts.useQuery("getAll")
  const { data: stats, isLoading: statsLoading } = q.analytics.useQuery("summary")

  const isLoading = usersLoading || postsLoading || statsLoading
  if (isLoading) return <p>Loading dashboard...</p>

  return (
    <div>
      <h2>Users: {users?.total}</h2>
      <h2>Posts: {posts?.total}</h2>
      <h2>Views: {stats?.totalViews}</h2>
    </div>
  )
}
```

## Infinite queries

For paginated lists with a "load more" pattern, use `useInfiniteQuery`. It wraps React Query's `useInfiniteQuery` with the same proxy transport and type safety:

```ts title="components/infinite-user-list.tsx"
"use client"

import { q } from "@/lib/query"

type UserList = { data: User[]; total: number; nextPage: number | null }

export function InfiniteUserList() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
    q.users.useInfiniteQuery("getAll", {
      variables: (page) => ({ page }),
      initialPageParam: 1,
      getNextPageParam: (lastPage) => lastPage.nextPage,
    })

  return (
    <div>
      {data?.pages.flatMap((page) =>
        page.data.map((user) => <div key={user.id}>{user.name}</div>)
      )}
      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? "Loading..." : "Load More"}
        </button>
      )}
    </div>
  )
}
```

`variables` is called with each page's parameter, so the parameter reaches the request. The first page's variables are part of the cache key — `["api", "users", "getAll", { page: 1 }, "infinite"]` here — so variables that select a different list, such as a filter, start it again from the first page.

> **Built-in infinite queries**
>
> `useInfiniteQuery` is accessed through the same dot-notation API — no need to import from `@tanstack/react-query` or wire up the caller manually. It uses the same proxy transport and shares the same defaults as `useQuery` and `useMutation`.

## Next steps

- [Middleware](/docs/advanced/middleware) — composable middleware for request interception, logging, and error handling
- [Singleton Pattern](/docs/advanced/singleton) — build the query client once and import it everywhere
- [createCaller](/docs/server-side/caller) — server-side alternative for RSC and Server Actions
