Skip to content
IC Reactor

useActorQuery

useActorQuery is a React hook for fetching data from Internet Computer canisters. It wraps TanStack Query’s useQuery with canister-specific functionality.

import { createActorHooks } from "@ic-reactor/react"
import { backend } from "./reactor"
const { useActorQuery } = createActorHooks(backend)
import { useActorQuery } from "./reactor"
function UserProfile({ userId }: { userId: string }) {
const { data, isPending, error } = useActorQuery({
functionName: "get_user",
args: [userId],
})
if (isPending) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return <div>{data?.name}</div>
}

UseActorQueryParameters extends react-query’s QueryObserverOptions, giving you access to all standard react-query options plus our custom actor options.

Option Type Description
functionName string The canister method to call
Option Type Default Description
args array | skipToken [] Arguments to pass to the method, or skipToken while they are not known
enabled boolean true Whether the query should run
staleTime number 0 Time in ms before data is considered stale
gcTime number 300000 Time in ms before unused data is garbage collected
refetchInterval number | false false Polling interval in ms
refetchOnWindowFocus boolean true Refetch when window regains focus
refetchOnMount boolean true Refetch when component mounts
refetchOnReconnect boolean true Refetch when network reconnects
retry number | boolean | function QueryClient’s Retry policy (update methods)
retryDelay number | function exponential Delay between retries
select function - Transform the data before returning
queryKey array - Additional query key segments
callConfig CallConfig - IC agent call configuration

Returns a TanStack Query result object:

Property Type Description
data TData | undefined The resolved data
error Error | null Error if query failed
status 'pending' | 'error' | 'success' Query status
isPending boolean True if no data yet
isError boolean True if query errored
isSuccess boolean True if query succeeded
isFetching boolean True if fetching (including background)
isRefetching boolean True if refetching
refetch function Manually trigger a refetch
dataUpdatedAt number Timestamp of last data update
import { useActorQuery } from "./reactor"
function Greeting() {
const { data, isPending, error } = useActorQuery({
functionName: "greet",
args: ["World"],
})
if (isPending) return <span>Loading…</span>
if (error) return <span>Error: {error.message}</span>
return <span>{data}</span>
}

When the arguments are not known yet, pass TanStack Query’s skipToken (re-exported by @ic-reactor/react) in their place. The query waits without calling the canister, and fetches as soon as args holds real arguments:

import { skipToken } from "@ic-reactor/react"
import { useActorQuery } from "./reactor"
function UserProfile({ userId }: { userId: string | undefined }) {
const { data } = useActorQuery({
functionName: "get_user",
// No `userId!`, no placeholder id, no `enabled`
args: userId ? [userId] : skipToken,
})
return <div>{data?.name}</div>
}

A skipped query has a cache entry of its own under its method’s key (at the canister callConfig names), which no call’s key shares: it shows no data until its arguments arrive, not even that of a query of the method made without arguments. refetch() has nothing to run until then, and TanStack Query answers it with a “Missing queryFn” error, so offer a refresh only once the arguments exist.

Use enabled when the arguments are known but the query should still wait, for a reason of your own:

import { useActorQuery, useAuth } from "./reactor"
function CurrentUser() {
const { isAuthenticated } = useAuth()
const { data } = useActorQuery({
functionName: "get_user",
args: [userId],
enabled: isAuthenticated, // Only fetch once signed in
})
return <div>{data?.name}</div>
}
import { useActorQuery } from "./reactor"
function UserName() {
// Extract only the name from the user object
const { data: userName } = useActorQuery({
functionName: "get_user",
args: [userId],
select: (user) => user?.name,
})
return <span>{userName}</span>
}
import { useActorQuery } from "./reactor"
function Settings() {
// Data stays fresh for 5 minutes
const { data } = useActorQuery({
functionName: "get_config",
args: [],
staleTime: 5 * 60 * 1000,
})
// Data is never stale
const { data: metadata } = useActorQuery({
functionName: "get_metadata",
args: [],
staleTime: Infinity,
})
return <pre>{JSON.stringify({ data, metadata })}</pre>
}
import { useActorQuery } from "./reactor"
function Price() {
// Refetch every 10 seconds
const { data } = useActorQuery({
functionName: "get_price",
args: [],
refetchInterval: 10_000,
})
return <span>{String(data)}</span>
}
import { useActorQuery } from "./reactor"
function Notifications() {
const { data, refetch, isFetching } = useActorQuery({
functionName: "get_notifications",
args: [],
})
return (
<div>
<NotificationList data={data} />
<button onClick={() => refetch()} disabled={isFetching}>
Refresh
</button>
</div>
)
}

A method that returns a Result resolves with its Ok value. Its Err arm is thrown as a CanisterError, which isCanisterError narrows, so a business logic failure is told apart from a network one:

import { isCanisterError } from "@ic-reactor/react"
import { useActorQuery } from "./reactor"
function Balance() {
// `get_balance` returns `{ Ok: nat } | { Err: BalanceError }`
const { data, error, isError } = useActorQuery({
functionName: "get_balance",
args: [principal.toText()],
})
if (isError) {
if (isCanisterError(error)) {
// Business logic error from canister
return (
<div>
Canister error [{error.code}]: {JSON.stringify(error.err)}
</div>
)
}
// Network or other error
return <div>Error: {error.message}</div>
}
return <div>Balance: {data}</div>
}
import { useActorQuery } from "./reactor"
function User() {
// TypeScript infers types from your canister interface
const { data } = useActorQuery({
functionName: "get_user", // ✅ Autocompletes valid methods
args: ["user-123"], // ✅ Type-checked
})
// data is typed as User | undefined
return <span>{data?.name}</span>
}
import { useActorQuery } from "./reactor"
function Data() {
// Add custom segments to the query key
const { data } = useActorQuery({
functionName: "get_data",
args: [],
queryKey: ["version", "2"],
})
// Full key: [canisterId, "get_data", "[]", "version", "2"]
return <pre>{JSON.stringify(data)}</pre>
}

The args segment is the JSON-serialized args array, so an empty args: [] still contributes the literal "[]". Custom queryKey segments are appended after it, never in place of the identity prefix.

import { skipToken } from "@ic-reactor/react"
import { useActorQuery } from "./reactor"
function UserPosts({ userId }: { userId: string }) {
// First query
const { data: user } = useActorQuery({
functionName: "get_user",
args: [userId],
})
// Dependent query - waits until the first one has the user's id
const { data: posts } = useActorQuery({
functionName: "get_user_posts",
args: user ? [user.id] : skipToken,
})
return <PostList posts={posts} />
}
useActorQuery<
M extends FunctionName<A>, // Method name
TSelected = ReturnType // Type after select
>(options: UseActorQueryParameters<A, M, T, TSelected>)

The types are automatically inferred from your actor interface, so you typically don’t need to specify them manually.