# createQuery

`createQuery` creates a reusable query object with pre-configured options. Perfect for route loaders, centralized query definitions, and cache manipulation outside React components.

## Import

```typescript
import { createQuery, createQueryFactory } from "@ic-reactor/react"
```

## Basic Usage

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

const userProfileQuery = createQuery(backend, {
  functionName: "get_profile",
  args: [currentUserId],
  staleTime: 5 * 60 * 1000, // 5 minutes
})
```

## Configuration

| Option         | Type                 | Default  | Description                                      |
| -------------- | -------------------- | -------- | ------------------------------------------------ |
| `functionName` | `string`             | Required | The canister method to call                      |
| `args`         | `Args`               | `[]`     | Arguments for the method                         |
| `select`       | `(data) => Selected` | -        | Transform data before returning                  |
| `staleTime`    | `number`             | `300000` | Time before data is stale (ms)                   |
| `queryKey`     | `QueryKey`           | Auto     | Custom query key segments                        |
| `callConfig`   | `CallConfig`         | -        | Canister, agent or effective target for the call |

The config also takes TanStack Query's query options. `networkMode`, `retry`,
`retryDelay` and `meta` decide how the canister call runs, so they apply to
`fetch()` and `prefetch()` as well as to `useQuery()`. The rest, such as
`enabled`, `placeholderData`, `initialData` and `gcTime`, apply to the hook
only. The config's `select` is applied by the query object itself, so
`fetch()`, `getCacheData()` and `useQuery()` all return the selected value;
`setData` still takes the raw value.

`callConfig` goes where it goes in `useActorQuery`: to the call and into the
query key. A `canisterId` there sends the query to another canister of the
same interface and caches it under that canister, so one reactor can back
query objects for several ledgers. The examples below use a `Reactor` of an
ICRC-1 ledger, built next to `backend`; being a raw `Reactor`, it takes an
account's owner as a `Principal`:

```typescript
// src/reactor.ts
import { defineReactor } from "@ic-reactor/react"
import {
  canisterId,
  idlFactory,
  type _SERVICE as LedgerService,
} from "./declarations/ledger"

export const { reactor: ledger } = defineReactor<LedgerService>({
  name: "ledger",
  idlFactory,
  canisterId,
})
```

```typescript
import { createQueryFactory } from "@ic-reactor/react"
import { ledger } from "./reactor"

const ckbtcBalance = createQueryFactory(ledger, {
  functionName: "icrc1_balance_of",
  callConfig: { canisterId: "mxzaz-hqaaa-aaaar-qaada-cai" },
})
```

`getQueryKey()`, `invalidate()` and the other cache controls act on that
entry, and a factory's own `getQueryKey()` is rooted at that canister. For a
whole reactor per canister, with every query and mutation on it, see
[`forCanister`](https://ic-reactor.b3pay.net/v3/reference/reactor#forcanister).

## Return Value

The factory returns an object with these utilities:

| Property           | Type                                 | Description                                        |
| ------------------ | ------------------------------------ | -------------------------------------------------- |
| `fetch`            | `() => Promise<T>`                   | Cache-first fetch — use in route loaders           |
| `prefetch`         | `() => Promise<void>`                | Fire-and-forget cache warm-up                      |
| `useQuery`         | `(options?) => UseQueryResult`       | React hook for components                          |
| `invalidate`       | `() => Promise<void>`                | Invalidate cache (refetches if active)             |
| `getQueryKey`      | `() => QueryKey`                     | Get the TanStack Query key                         |
| `getCacheData`     | `(select?) => T`                     | Read from cache without fetching                   |
| `setData`          | `(updater) => T \| undefined`        | Write raw data into cache                          |
| `cancel`           | `() => Promise<void>`                | Cancel this query's fetch in flight                |
| `reset`            | `() => Promise<void>`                | Reset to initial state; mounted hooks refetch      |
| `optimisticUpdate` | `(updater) => Promise<{ rollback }>` | Cancel, write `updater(cached)`, return a rollback |

---

## Examples

### TanStack Router Integration

Use factories with TanStack Router's file-based routing and loaders:

```tsx
// routes/users/$userId.tsx
import { createFileRoute } from "@tanstack/react-router"
import { createQueryFactory } from "@ic-reactor/react"
import { backend } from "../../reactor"
import { redirect } from "@tanstack/react-router"

// Create factory (can be in a separate file)
const getUserQuery = createQueryFactory(backend, {
  functionName: "get_user",
  select: (user) => user.profile,
  staleTime: 5 * 60 * 1000,
})

export const Route = createFileRoute("/users/$userId")({
  // Prefetch in loader - runs before component mounts
  loader: async ({ params }) => {
    const user = await getUserQuery([params.userId]).fetch()

    if (!user) {
      throw redirect({ to: "/404" })
    }

    return { user }
  },

  // Optional: Define pending component
  pendingComponent: () => <div>Loading user...</div>,

  // Main component
  component: UserPage,
})

function UserPage() {
  const params = Route.useParams()

  // Data is already cached from loader - instant render! The type still
  // includes `undefined`, as it does for every `useQuery()`.
  const { data: user, isRefetching } = getUserQuery([params.userId]).useQuery()

  return (
    <div>
      <h1>{user?.name}</h1>
      {isRefetching && <span>Refreshing...</span>}
    </div>
  )
}
```

### TanStack Router with Route Context

Pass the reactor via router context for better organization:

```tsx
// routes/__root.tsx
import { createRootRouteWithContext } from "@tanstack/react-router"
import type { backend } from "../reactor"

interface RouterContext {
  backend: typeof backend
}

export const Route = createRootRouteWithContext<RouterContext>()({
  component: RootLayout,
})
```

```tsx
// router.tsx
import { createRouter } from "@tanstack/react-router"
import { backend } from "./reactor"
import { routeTree } from "./routeTree.gen"

export const router = createRouter({
  routeTree,
  context: { backend },
  defaultPreloadStaleTime: 0, // Let TanStack Query handle caching
})
```

```tsx
// routes/posts.tsx
import { createFileRoute } from "@tanstack/react-router"
import { createQuery } from "@ic-reactor/react"

export const Route = createFileRoute("/posts")({
  loader: async ({ context }) => {
    const postsQuery = createQuery(context.backend, {
      functionName: "get_posts",
    })
    return { posts: await postsQuery.fetch() }
  },
  component: PostsPage,
})
```

### React Router 6/7 Integration

```tsx
// routes.tsx
import { createBrowserRouter, type LoaderFunctionArgs } from "react-router-dom"
import { createQueryFactory } from "@ic-reactor/react"
import { backend } from "./reactor"

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

async function loader({ params }: LoaderFunctionArgs) {
  if (!params.userId) throw new Response("Not Found", { status: 404 })
  return { user: await getUserQuery([params.userId]).fetch() }
}

export const router = createBrowserRouter([
  {
    path: "/users/:userId",
    loader,
    Component: UserPage,
  },
])

// UserPage.tsx
import { skipToken } from "@ic-reactor/react"
import { useLoaderData, useParams } from "react-router-dom"

function UserPage() {
  // Initial data from loader
  const { user: initialUser } = useLoaderData() as Awaited<
    ReturnType<typeof loader>
  >
  const { userId } = useParams()

  // Subscribe to updates
  const { data: user } = getUserQuery(userId ? [userId] : skipToken).useQuery({
    initialData: initialUser,
  })

  return <Profile user={user} />
}
```

### Select Transformation

Transform data at the factory level:

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

// Only get what you need
const userNameQuery = createQuery(backend, {
  functionName: "get_user",
  args: [userId],
  select: (user) => user.name, // Only extract name
})

function UserName() {
  // data is just the name string
  const { data: userName } = userNameQuery.useQuery()

  return <span>{userName}</span>
}
```

### Chained Select in Hook

Chain additional transformations in the hook:

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

const userQuery = createQuery(backend, {
  functionName: "get_user",
  args: [userId],
  select: (user) => user.profile, // First transformation
})

function Avatar() {
  // Further transform in the component
  const { data: avatarUrl } = userQuery.useQuery({
    select: (profile) => profile.avatarUrl, // Chained transformation
  })

  return <img src={avatarUrl} alt="" />
}
```

### Conditional Fetching

Use `enabled` option for conditional queries:

```tsx
import { createQuery } from "@ic-reactor/react"
import { backend, useAuth } from "./reactor"

const myProfileQuery = createQuery(backend, {
  functionName: "get_my_profile",
  args: [],
})

function ProtectedComponent() {
  const { isAuthenticated } = useAuth()

  const { data } = myProfileQuery.useQuery({
    enabled: isAuthenticated, // Only fetch when authenticated
  })

  return isAuthenticated ? <ProfileCard profile={data} /> : <LoginPrompt />
}
```

### Cache Manipulation

Read and manipulate cache outside React:

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

const userQuery = createQuery(backend, {
  functionName: "get_user",
  args: [userId],
})

async function manageCache() {
  // Read from cache (returns undefined if not cached)
  const cachedUser = userQuery.getCacheData()

  // Read with transformation
  const cachedName = userQuery.getCacheData((user) => user.name)

  // Force invalidation (refetches if query is active)
  await userQuery.invalidate()

  // Write directly into cache
  userQuery.setData(updatedUser)

  // Or use an updater function. `old` is undefined when nothing is cached yet.
  userQuery.setData((old) => (old ? { ...old, name: "Alice" } : old))

  // Cancel the fetch in flight, or reset the entry to its initial state
  await userQuery.cancel()
  await userQuery.reset()
}
```

### Optimistic Updates

`optimisticUpdate` cancels the query's fetch in flight, so an older answer
cannot land on top, writes what the updater returns for the cached value, and
resolves with a rollback. Return it from a mutation's `onMutate`:

```typescript
import { createMutation } from "@ic-reactor/react"
import { backend } from "./reactor"
import { getUserQuery } from "./queries"

const renameUser = createMutation(backend, {
  functionName: "rename_user",
  onMutate: ([userId, name]) =>
    getUserQuery([userId]).optimisticUpdate((user) => ({ ...user, name })),
  onError: (_error, _args, update) => update?.rollback(),
  invalidateQueries: [getUserQuery],
})
```

The updater gets the raw data, before the config's `select`, and is typed
from the method. It is not called when nothing is cached, and `rollback()`
then does nothing. `rollback()` writes back the replaced value with its
original fetch time, and invalidates it again if it was invalidated. After a
sign-in or sign-out it does nothing: the value was the previous principal's.
Refetch the query once the mutation settles, as `invalidateQueries` does
here, since the fetch `optimisticUpdate` cancels may be one an invalidation
started.

`cancel()`, `reset()` and `optimisticUpdate()` act on this query's own entry
only. `invalidate()` matches by key prefix, so on a query object without args
it also reaches the same method's queries with args; these three do not.

### Prefetching on Hover

Prefetch data when the user hovers over a link. Use `prefetch()` (fire-and-forget)
rather than `fetch()` here — you don't need to `await` a result, just warm the cache:

```tsx
import { Link } from "@tanstack/react-router"
import { getUserQuery } from "./queries"

function UserLink({ userId }: { userId: string }) {
  const handleMouseEnter = () => {
    // Fire-and-forget cache warm-up
    getUserQuery([userId]).prefetch()
  }

  return (
    <Link to={`/users/${userId}`} onMouseEnter={handleMouseEnter}>
      View User
    </Link>
  )
}
```

---

## createQueryFactory

For dynamic arguments (like route params), use the factory variant:

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

const getUserQuery = createQueryFactory(backend, {
  functionName: "get_user",
  staleTime: 5 * 60 * 1000,
})

// Create query instances with different args
const aliceQuery = getUserQuery(["alice"])
const bobQuery = getUserQuery(["bob"])

// Use in components
function Friends() {
  const { data: alice } = aliceQuery.useQuery()
  const { data: bob } = bobQuery.useQuery()

  return (
    <ul>
      <li>{alice?.name}</li>
      <li>{bob?.name}</li>
    </ul>
  )
}
```

### Every Instance at Once

The factory function itself has `getQueryKey()` and `invalidate()`. They cover
every query it returns, whatever the args:

```typescript
import { createMutation } from "@ic-reactor/react"
import { backend } from "./reactor"
import { getUserQuery } from "./queries"

async function refreshUsers() {
  // Refetch every user on screen
  await getUserQuery.invalidate()

  // The key prefix those queries share, for QueryClient calls of your own
  return backend.queryClient.getQueriesData({
    queryKey: getUserQuery.getQueryKey(),
  })
}

// Or hand the factory to a mutation
const renameUser = createMutation(backend, {
  functionName: "rename_user",
  invalidateQueries: [getUserQuery],
})
```

| Property      | Type                  | Description                                       |
| ------------- | --------------------- | ------------------------------------------------- |
| `getQueryKey` | `() => QueryKey`      | The method's key prefix, shared by every instance |
| `invalidate`  | `() => Promise<void>` | Invalidate every instance (active ones refetch)   |

The prefix is the canister (the config's `callConfig.canisterId`, else the
reactor's) and the method, so it also matches a query of the same method made
elsewhere, such as with `useActorQuery`. A config `queryKey` comes after the
args in each instance's key, so it does not narrow the prefix.

### Waiting for Args

The factory also takes TanStack Query's `skipToken`, re-exported by
`@ic-reactor/react`, in place of args. It returns a query with only
`useQuery()`, which waits without calling the canister, so a component whose
args are not known yet needs no placeholder args and no `enabled`:

```tsx
import { createQueryFactory, skipToken } from "@ic-reactor/react"
import type { Principal } from "@icp-sdk/core/principal"
import { ledger } from "./reactor"

// `ledger` is a Reactor of the ICRC-1 ledger, so an account's owner is a
// Principal (a DisplayReactor's takes the principal's text)
const getBalance = createQueryFactory(ledger, {
  functionName: "icrc1_balance_of",
})

function Balance({ owner }: { owner?: Principal }) {
  const { data } = getBalance(
    owner ? [{ owner, subaccount: [] }] : skipToken
  ).useQuery()
  return <span>{data?.toString() ?? "…"}</span>
}
```

The skipped query has a cache entry of its own under the factory's
`getQueryKey()` prefix, which no call's key shares. Its imperative members
(`fetch()`, `invalidate()`, the cache controls) are left out, since there is
no call or entry until the args are known: check the args before reaching for
them. `createSuspenseQueryFactory` does not take `skipToken`.

### Factory Caching

Factory instances are cached internally:

```typescript
import { getUserQuery } from "./queries"

// These return the SAME object instance
const query1 = getUserQuery(["alice"])
const query2 = getUserQuery(["alice"])

console.log(query1 === query2) // true
```

The cache holds the 256 most recently used argument sets. That covers any
realistic working set, but a factory called with an open-ended range of
arguments — one query per principal in a long-lived session, a
search-as-you-type filter, a cursor-keyed list — will eventually evict the
oldest entries rather than grow without bound.

Eviction costs memoization, not correctness: a query object is a closure over
the reactor and its config, so one rebuilt after eviction behaves identically
and only its reference identity differs. It matters only if you compare
instances or put one in a dependency array _and_ cycle through more than 256
distinct argument sets.

---

## Notes

**Caution:** An update method passed here runs on the canister again on every refetch
  (mount, window focus, reconnect, invalidation), retry and uncached `fetch()`,
  each time as a new call. Use it only for an update method that is safe to
  repeat, and use `createMutation` for a method that changes state. With no
  `retry` in the factory config or the hook's options, an update method retries
  only a `SysTransient` rejection, which proves the call never ran. See [Update
  Methods in Queries](https://ic-reactor.b3pay.net/v3/framework/queries#update-methods-in-queries).

**Tip:** Use `createQueryFactory` when args come from route params or user input. Use
  `createQuery` when args are known at definition time.

**Note:** `fetch()` is cache-first, like TanStack Query's `ensureQueryData`: it returns
  the cached value whenever there is one, even a stale or invalidated one, and
  calls the canister only when nothing is cached. That suits loaders. For the
  canister's current value, use the reactor's `callMethod()`; see
  [Reactor](https://ic-reactor.b3pay.net/v3/reference/reactor).

**Note:** A sign-in or sign-out while `fetch()` is in flight does not reject it. The
  previous identity's answer is dropped, and `fetch()` runs again for the new
  identity and resolves with that answer, so a loader never sees TanStack's
  `CancelledError` for it. `prefetch()` runs again the same way: when that run
  succeeds, the cache holds the new identity's answer by the time it resolves.
  It still never rejects.

**Caution:** If you need Suspense-based loading (where `data` is never undefined), use
  [createSuspenseQuery](https://ic-reactor.b3pay.net/v3/reference/factories/createsuspensequery) instead.

## See Also

- [createSuspenseQuery](https://ic-reactor.b3pay.net/v3/reference/factories/createsuspensequery) — Suspense version
- [createInfiniteQuery](https://ic-reactor.b3pay.net/v3/reference/factories/createinfinitequery) — Paginated queries
- [useActorQuery](https://ic-reactor.b3pay.net/v3/reference/createactorhooks/useactorquery) — Direct hook usage
- [Queries Guide](https://ic-reactor.b3pay.net/v3/framework/queries) — Query patterns