Skip to content
IC Reactor

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 { createQuery, createQueryFactory } from "@ic-reactor/react"
import { createQuery } from "@ic-reactor/react"
import { backend } from "./reactor"
const userProfileQuery = createQuery(backend, {
functionName: "getProfile",
args: [currentUserId],
staleTime: 5 * 60 * 1000, // 5 minutes
})
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

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

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

// 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: "getUser",
select: (result) => result.user,
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("/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!
const { data: user, isRefetching } = getUserQuery([params.userId]).useQuery()
return (
<div>
<h1>{user.name}</h1>
{isRefetching && <span>Refreshing...</span>}
</div>
)
}

Pass the reactor via router context for better organization:

router.tsx
import {
createRouter,
createRootRouteWithContext,
} from "@tanstack/react-router"
import { backend } from "./reactor"
interface RouterContext {
backend: typeof backend
}
const rootRoute = createRootRouteWithContext<RouterContext>()({
component: RootLayout,
})
export const router = createRouter({
routeTree,
context: { backend },
defaultPreloadStaleTime: 0, // Let TanStack Query handle caching
})
// 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: "getPosts",
})
return { posts: await postsQuery.fetch() }
},
component: PostsPage,
})
routes.tsx
import { createBrowserRouter, json } from "react-router-dom"
import { createQueryFactory } from "@ic-reactor/react"
import { backend } from "./reactor"
const getUserQuery = createQueryFactory(backend, {
functionName: "getUser",
})
export const router = createBrowserRouter([
{
path: "/users/:userId",
loader: async ({ params }) => {
const user = await getUserQuery([params.userId!]).fetch()
return json({ user })
},
Component: UserPage,
},
])
// UserPage.tsx
import { useLoaderData, useParams } from "react-router-dom"
function UserPage() {
// Initial data from loader
const { user: initialUser } = useLoaderData() as { user: User }
const { userId } = useParams()
// Subscribe to updates
const { data: user } = getUserQuery([userId!]).useQuery({
initialData: initialUser,
})
return <Profile user={user} />
}

Transform data at the factory level:

// Only get what you need
const userNameQuery = createQuery(backend, {
functionName: "getUser",
args: [userId],
select: (user) => user.name, // Only extract name
})
// In component - data is just the name string
const { data: userName } = userNameQuery.useQuery()

Chain additional transformations in the hook:

const userQuery = createQuery(backend, {
functionName: "getUser",
args: [userId],
select: (user) => user.profile, // First transformation
})
// Further transform in component
const { data: avatarUrl } = userQuery.useQuery({
select: (profile) => profile.avatarUrl, // Chained transformation
})

Use enabled option for conditional queries:

const sensitiveDataQuery = createQuery(backend, {
functionName: "getSensitiveData",
args: [],
})
function ProtectedComponent() {
const { isAuthenticated } = useAuth()
const { data } = sensitiveDataQuery.useQuery({
enabled: isAuthenticated, // Only fetch when authenticated
})
return isAuthenticated ? <Data data={data} /> : <LoginPrompt />
}

Read and manipulate cache outside React:

const userQuery = createQuery(backend, {
functionName: "getUser",
args: [userId],
})
// 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 (optimistic updates)
userQuery.setData(updatedUser)
// Or use an updater function
userQuery.setData((old) => ({ ...old, name: "Alice" }))

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:

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>
)
}

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

import { createQueryFactory } from "@ic-reactor/react"
const getUserQuery = createQueryFactory(backend, {
functionName: "getUser",
staleTime: 5 * 60 * 1000,
})
// Create query instances with different args
const aliceQuery = getUserQuery(["alice"])
const bobQuery = getUserQuery(["bob"])
// Use in components
const { data: alice } = aliceQuery.useQuery()
const { data: bob } = bobQuery.useQuery()

Factory instances are cached internally:

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