# Query Caching

IC Reactor uses [TanStack Query](https://tanstack.com/query) under the hood for intelligent caching and data synchronization. This guide covers everything you need to know about how caching works.

## How It Works

When you call `useActorQuery`, IC Reactor:

1. **Generates a unique cache key** based on the canister ID, function name, and arguments
2. **Returns cached data immediately** if available
3. **Fetches fresh data in the background** if data is stale
4. **Automatically updates the UI** when new data arrives

This pattern is often called "stale-while-revalidate" and provides the best user experience by showing data immediately while keeping it fresh.

## Cache Keys

Cache keys are automatically generated from your query parameters:

```typescript
import { useActorQuery } from "./reactor/hooks"

function Users() {
  // Query key: ['rrkah-fqaaa...', 'get_user', '["user-123"]']
  const { data: user123 } = useActorQuery({
    functionName: "get_user",
    args: ["user-123"],
  })

  // Query key: ['rrkah-fqaaa...', 'get_user', '["user-456"]']
  const { data: user456 } = useActorQuery({
    functionName: "get_user",
    args: ["user-456"],
  })
}
```

Each unique combination of canister ID, function name, and arguments creates a separate cache entry. The whole `args` array is serialized into a **single** string segment, `argsJson` — arguments are never spread into separate elements. It is JSON with BigInt values written as decimal strings and the keys of every plain object sorted, so `{ b, a }` and `{ a, b }` share an entry, and a bare `JSON.stringify(args)` does not match it once a record's fields are out of alphabetical order. A blob is written as a tag followed by its lowercase hex, so a `Uint8Array`, a `number[]` and a `DisplayReactor`'s hex text holding the same bytes share an entry too. A `DisplayReactor` writes each `opt` and variant in one form, whichever it was given in, so an `opt` given bare or as `[value]`, none given as `null`, `undefined` or `[]`, and a variant given with or without `_type` share an entry. A `vec record { text; T }` given to it as an object or a `Map` is written as its entries in order, since the order is part of what it sends. A record or variant given to it as a class instance is written as the plain object its codec sends, a field its class supplies through a getter included. Fields a record does not declare are left out and every value of `reserved` is written as `null`, since neither is sent, and a `DisplayReactor` writes a float or an integer of 32 bits or fewer given as text as its number, and a `Principal` as its text. An argument the reactor refuses, such as `undefined` where Candid `null` is required or a bigint where a `DisplayReactor` takes text, is written behind a tag of its own, so the call fails instead of being answered from the cache entry of an argument it takes.

The keys above are a `Reactor`'s: `[canisterId, functionName, argsJson]`. A `DisplayReactor` adds a `{ transform: "display" }` segment after the function name — `['rrkah-fqaaa...', 'get_user', { transform: 'display' }, '["user-123"]']` — so a `Reactor` and a `DisplayReactor` over one canister never share a cache entry. Build keys with `generateQueryKey` (below) or a query object's `getQueryKey()` rather than by hand.

### Custom Query Keys

You can add custom keys for more granular cache control:

```typescript
import { useActorQuery } from "./reactor/hooks"

function VersionedData() {
  const { data } = useActorQuery({
    functionName: "get_data",
    args: [],
    queryKey: ["version", "2"], // Spread onto the end of the auto-generated key
  })
  // Full key: ['rrkah-fqaaa...', 'get_data', '[]', 'version', '2']
}
```

**Note:** An empty `args: []` is still truthy, so it contributes the literal segment
  `"[]"`. Only omitting `args` entirely drops that segment.

### Accessing Query Keys

Use `generateQueryKey` to get the cache key for any query:

```typescript
import { Principal } from "@icp-sdk/core/principal"
import { backend } from "./reactor"

const queryKey = backend.generateQueryKey(
  {
    functionName: "get_user",
    args: ["user-123"],
  },
  {
    canisterId: otherCanisterId,
    effectiveCanisterId: Principal.fromText("aaaaa-aa"), // the management canister
  }
)
// [otherCanisterId, 'get_user', { effectiveTarget: { canisterId: 'aaaaa-aa' } }, '["user-123"]']
```

Note that `callConfig.canisterId` **replaces** the reactor's own canister ID in
the first segment, and the effective target is wrapped in an `effectiveTarget`
object. That segment is dropped entirely when the effective canister ID equals
the resolved canister ID, so a call that targets only its own canister produces
the plain `[canisterId, functionName, argsJson]` key (with the
`{ transform: "display" }` segment before `argsJson` on a `DisplayReactor`).

If a query uses `callConfig`, pass the same `callConfig` when generating,
reading, or invalidating that cache key. Cache entries are partitioned by the
resolved target canister, the optional effective target, and the agent.

A query sent through `callConfig.agent` (another identity, the anonymous agent,
another network) gets an `{ agent: n }` segment after the transform segment and
before `argsJson`, where `n` numbers that agent within the running process. So
the same query through that agent and through the `ClientManager`'s own agent
are two cache entries, and each is fetched from the canister. Keys without an
override, or whose override is the manager's own agent, have no such segment.
`invalidateQueries({ functionName })` still matches both entries, and
`updateAgent()` still sweeps the override's entries with the rest of the
canister's. Each process counts from a random start, so an override entry in
a dehydrated server cache, or in a cache persisted and restored later, is not
found by another process's agents.

**Caution:** Query keys name the canister, not the `ClientManager`. Two `ClientManager`s on
  the same canister (two identities, or a local replica and mainnet) whose
  reactors share one `QueryClient` share cache entries, and one manager's
  `updateAgent()` sweeps the other's entries. Give each `ClientManager` its own
  `QueryClient`: `defineReactor` does that by default when it builds the
  manager.

## Stale Time

`staleTime` determines **how long data is considered fresh**. Fresh data is never refetched:

```typescript
import { useActorQuery } from "./reactor/hooks"

function AppInfo() {
  // Data is fresh for 5 minutes
  const { data: config } = useActorQuery({
    functionName: "get_config",
    args: [],
    staleTime: 1000 * 60 * 5, // 5 minutes
  })

  // Data is never stale (static data that never changes)
  const { data: metadata } = useActorQuery({
    functionName: "get_metadata",
    args: [],
    staleTime: Infinity,
  })

  // Data is immediately stale (default behavior)
  const { data: price } = useActorQuery({
    functionName: "get_price",
    args: [],
    staleTime: 0, // Refetch on every mount
  })
}
```

**Tip:** Set a longer `staleTime` for data that doesn't change often (like
  configuration or metadata) to reduce unnecessary network requests.

## Garbage Collection Time

`gcTime` determines **how long unused data stays in cache** before being garbage collected:

```typescript
import { useActorQuery } from "./reactor/hooks"

function Cached({ userId }: { userId: string }) {
  // Keep unused data for 10 minutes (default is 5 minutes)
  const { data: user } = useActorQuery({
    functionName: "get_user",
    args: [userId],
    gcTime: 1000 * 60 * 10,
  })

  // Keep data forever
  const { data: config } = useActorQuery({
    functionName: "get_config",
    args: [],
    gcTime: Infinity,
  })
}
```

The difference between `staleTime` and `gcTime`:

- **Stale data** can be served immediately while fetching fresh data
- **Garbage collected data** is removed entirely and must be fetched again

## Automatic Refetching

### Refetch Interval

Automatically refetch data at a set interval:

```typescript
import { useActorQuery } from "./reactor/hooks"

function Prices() {
  // Refetch every 10 seconds
  const { data: price } = useActorQuery({
    functionName: "get_price",
    args: [],
    refetchInterval: 1000 * 10,
  })

  // Only refetch when window is focused
  const { data: focusedPrice } = useActorQuery({
    functionName: "get_price",
    args: [],
    refetchInterval: 1000 * 10,
    refetchIntervalInBackground: false,
  })
}
```

### Refetch Triggers

```typescript
import { useActorQuery } from "./reactor/hooks"

function Notifications() {
  const { data } = useActorQuery({
    functionName: "get_notifications",
    args: [],
    // Refetch when window regains focus (default: true)
    refetchOnWindowFocus: true,
    // Refetch when component mounts (default: true)
    refetchOnMount: true,
    // Refetch when reconnecting to network (default: true)
    refetchOnReconnect: true,
  })
}
```

## Manual Invalidation

### Using Query Objects

A query object and a query factory invalidate their own queries:

```tsx
import { createQueryFactory } from "@ic-reactor/react"
import { backend } from "./reactor"

const getUser = createQueryFactory(backend, { functionName: "get_user" })

function RefreshButton({ userId }: { userId: string }) {
  const handleRefresh = () => {
    // One user's query
    void getUser([userId]).invalidate()
  }

  const handleRefreshAll = () => {
    // Every getUser query this factory made, whatever the args
    void getUser.invalidate()
  }

  return (
    <>
      <button onClick={handleRefresh}>Refresh</button>
      <button onClick={handleRefreshAll}>Refresh all users</button>
    </>
  )
}
```

`getUser.getQueryKey()` is the key prefix those queries share, for any other
QueryClient call. For the client itself use `backend.queryClient`: the hooks
bind to the reactor's own client, and `useQueryClient()` throws when there is
no `QueryClientProvider`, which is optional.

### Invalidate All Canister Queries

```typescript
import { backend } from "./reactor"

async function refreshEverything() {
  // Invalidate ALL queries for this canister
  await backend.invalidateQueries()
}
```

`invalidateQueries` returns TanStack Query's `Promise<void>`, which resolves
once the active queries it matched have refetched; a failed refetch does not
reject it. Await it when the next step should read the refetched data, and
prefix a call you do not wait for with `void` if you lint with
`no-floating-promises`.

### Invalidate by Function Name

```typescript
import { backend } from "./reactor"

async function refreshUsers() {
  // Invalidate all get_user queries (any args)
  await backend.invalidateQueries({ functionName: "get_user" })

  // Invalidate get_user queries for a specific overridden target canister
  await backend.invalidateQueries(
    { functionName: "get_user" },
    { canisterId: otherCanisterId }
  )
}
```

### Invalidate Specific Query

```typescript
import { backend } from "./reactor"

async function refreshUser() {
  // Invalidate get_user query for specific user
  await backend.invalidateQueries(
    {
      functionName: "get_user",
      args: ["user-123"],
    },
    {
      canisterId: otherCanisterId,
    }
  )
}
```

### Invalidate by Custom Query Key

```typescript
import { backend } from "./reactor"

async function refreshVersionedData() {
  // Invalidate query with custom key
  await backend.invalidateQueries({
    functionName: "get_data",
    queryKey: ["version", "2"],
  })
}
```

## Refetch After Mutation

### Using invalidateQueries

The simplest way to refetch related data after a mutation:

```typescript
import { useActorMutation } from "./reactor/hooks"

function useCreatePost() {
  return useActorMutation({
    functionName: "create_post",
    invalidateQueries: [
      { functionName: "get_posts" }, // every get_posts query, whatever its args
      { functionName: "get_post_count" },
    ],
  })
}
```

An entry can also be a query object or a query factory (`[postsQuery]`,
`[getUser]`), or a query key. The invalidation is awaited before `onSuccess`
runs. See [Mutations](https://ic-reactor.b3pay.net/v3/framework/mutations#invalidating-queries-after-mutation).

### Using onSuccess

For more control, use `onSuccess`. Here the new post goes straight into the
cached list instead of being refetched:

```typescript
import { createQuery } from "@ic-reactor/react"
import { backend } from "./reactor"
import { useActorMutation } from "./reactor/hooks"

const postsQuery = createQuery(backend, { functionName: "get_posts" })

function useCreatePost() {
  return useActorMutation({
    functionName: "create_post",
    onSuccess: (newPost) => {
      // `old` is undefined when the list is not cached yet
      postsQuery.setData((old) => (old ? [...old, newPost] : old))
    },
  })
}
```

## Optimistic Updates

Update the UI immediately, then sync with the server:

```tsx
import { createQueryFactory } from "@ic-reactor/react"
import { backend } from "./reactor"
import { useActorMutation } from "./reactor/hooks"

const getPost = createQueryFactory(backend, { functionName: "get_post" })

function LikeButton({ postId }: { postId: string }) {
  const { mutate } = useActorMutation({
    functionName: "like_post",
    // Cancel the post's fetch in flight, write the new value, keep a rollback
    onMutate: ([postId]) =>
      getPost([postId]).optimisticUpdate((post) => ({
        ...post,
        likes: post.likes + 1n,
      })),
    // Roll back on error
    onError: (_error, _args, update) => update?.rollback(),
    // Always refetch after error or success
    onSettled: (_data, _error, [postId]) => getPost([postId]).invalidate(),
  })

  return <button onClick={() => mutate([postId])}>Like</button>
}
```

### Query Object Cache Controls

Every query object (`createQuery`, `createSuspenseQuery`, the infinite
variants and factory instances) acts on its own cache entry:

| Method                      | What it does                                                                                    |
| --------------------------- | ----------------------------------------------------------------------------------------------- |
| `optimisticUpdate(updater)` | Cancels the fetch in flight, writes `updater(cached)`, resolves with `{ rollback() }`           |
| `cancel()`                  | Cancels the fetch in flight; the entry keeps the value it held before                           |
| `reset()`                   | Clears the entry (or restores `initialData`); a mounted hook refetches, a suspense one suspends |
| `setData(updater)`          | Writes a raw value, or `updater(cached)`, with no cancel and no rollback (not on infinite ones) |
| `invalidate()`              | Marks it stale; a mounted hook refetches                                                        |

`cancel()`, `reset()` and `optimisticUpdate()` match the query's key exactly,
so other args of the same method are left alone. An infinite query's
`optimisticUpdate` gets and returns its `{ pages, pageParams }`.

## QueryClient Configuration

Set global defaults for all queries:

```typescript
import { QueryClient } from "@tanstack/react-query"

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60, // 1 minute default
      gcTime: 1000 * 60 * 5, // 5 minutes
      retry: 2, // Retry failed queries twice
      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
      refetchOnWindowFocus: true,
    },
    mutations: {
      retry: 1,
    },
  },
})
```

### Smart Retry Logic

Retrying helps only when the failure could change next time — a transport
fault, a transient replica rejection, an HTTP 5xx, 408 or 429 answer. A
canister `Err` variant, a validation failure, a Candid encode error, or an
HTTP 4xx refusal such as an expired delegation's is deterministic, and
retrying it spends several attempts and seconds of backoff to reach the same
failure. IC Reactor
ships this classification as `reactorRetry`, which is already the query default
in the `QueryClient` that `defineReactor` creates. Opt in on a `QueryClient`
you construct yourself:

```typescript
import { reactorRetry } from "@ic-reactor/react"
import { QueryClient } from "@tanstack/react-query"

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: reactorRetry,
    },
  },
})
```

For custom attempt counts, build your own predicate on
`isRetryableReactorError` — see
[Error Handling — Retry Configuration](https://ic-reactor.b3pay.net/v3/guides/error-handling#retry-configuration).

An update method run through a query is narrowed further: without a `retry` of
its own, it retries only a `SysTransient` rejection, since any other retry
could execute it a second time. See
[Update Methods in Queries](https://ic-reactor.b3pay.net/v3/framework/queries#update-methods-in-queries).

## Prefetching

Prefetch data before it's needed:

```tsx
import { Link } from "react-router-dom"
import { createQuery, createQueryFactory } from "@ic-reactor/react"
import { backend } from "./reactor"

const postsQuery = createQuery(backend, { functionName: "get_posts" })
const getPost = createQueryFactory(backend, { functionName: "get_post" })

// In a route loader
async function postsLoader() {
  await postsQuery.prefetch()
  return null
}

// Or on hover
function PostLink({ postId }: { postId: string }) {
  const handleMouseEnter = () => {
    void getPost([postId]).prefetch()
  }

  return (
    <Link to={`/posts/${postId}`} onMouseEnter={handleMouseEnter}>
      View Post
    </Link>
  )
}
```

`prefetch()` never rejects, and it fetches again for the new principal when a
sign-in or sign-out cancels it mid-flight, as `fetch()` does. A prefetch you
write against the reactor's `QueryClient` yourself gets no such second chance:
wrap it in `clientManager.fetchAcrossIdentitySwitch`, or the entry can end up
holding the previous principal's data.

```ts
import { backend } from "./reactor"

async function prefetchPosts() {
  await backend.clientManager.fetchAcrossIdentitySwitch(() =>
    backend.queryClient.prefetchQuery(
      backend.getQueryOptions({ functionName: "get_posts", args: [] })
    )
  )
}
```

## Initial Data

Provide initial data while fresh data loads:

```typescript
import { useActorQuery } from "./reactor/hooks"

function Settings() {
  const { data } = useActorQuery({
    functionName: "get_settings",
    args: [],
    initialData: {
      theme: "light",
      notifications: true,
    },
    // Don't count initial data as fresh
    initialDataUpdatedAt: 0,
  })
}
```

## Placeholder Data

Show placeholder data that doesn't get cached:

```typescript
import { useActorQuery } from "./reactor/hooks"

function PostPreview({ postId }: { postId: string }) {
  const { data } = useActorQuery({
    functionName: "get_post",
    args: [postId],
    placeholderData: {
      id: postId,
      title: "Loading...",
      content: "",
      likes: 0n,
    },
  })
}
```

## Debug Caching

Enable React Query DevTools to inspect the cache:

```tsx
import { QueryClientProvider } from "@tanstack/react-query"
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}
```

The DevTools show:

- All cached queries
- Their status (fresh, stale, fetching)
- Cache keys
- Last updated time
- Ability to manually trigger refetches

## Further Reading

- [TanStack Query Documentation](https://tanstack.com/query/latest) — Complete TanStack Query docs
- [Queries](https://ic-reactor.b3pay.net/v3/framework/queries) — IC Reactor query patterns
- [Mutations](https://ic-reactor.b3pay.net/v3/framework/mutations) — IC Reactor mutation patterns