Skip to content

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
})
OptionTypeDefaultDescription
functionNamestringRequiredThe canister method to call
argsArgs[]Arguments for the method
select(data) => Selected-Transform data before returning
staleTimenumber300000Time before data is stale (ms)
queryKeyQueryKeyAutoCustom query key segments

The factory returns an object with these utilities:

PropertyTypeDescription
fetch() => Promise<T>Fetch data (cache-first, for loaders)
useQuery(options?) => UseQueryResultReact hook for components
invalidate() => Promise<void>Invalidate cache (refetches if active)
getQueryKey() => QueryKeyGet the TanStack Query key
getCacheData(select?) => TRead from cache without fetching

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()
// Get query key for manual operations
const queryKey = userQuery.getQueryKey()
queryClient.setQueryData(queryKey, updatedUser)

Prefetch data when user hovers over a link:

function UserLink({ userId }: { userId: string }) {
const handleMouseEnter = async () => {
// Prefetch on hover
await getUserQuery([userId]).fetch()
}
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