Queries
Query Basics
Section titled “Query Basics”A query is a declarative dependency on an asynchronous data source from an IC canister that is tied to a unique key. A query can be used to fetch any data from your canisters using query or update methods.
To subscribe to a query in your components, call the useActorQuery hook with at least:
- A function name that exists on your actor
- Optionally, arguments to pass to that function
import { useActorQuery } from "../reactor/hooks"
function App() { const info = useActorQuery({ functionName: "getUser", args: ["user-123"], })}The unique key is automatically generated based on your canister ID, function name, and arguments. This key is used internally for refetching, caching, and sharing your queries throughout your application.
Query Results
Section titled “Query Results”The query result returned by useActorQuery contains all of the information about the query that you’ll need for templating and any other usage of the data:
const result = useActorQuery({ functionName: "getUser", args: ["user-123"],})The result object contains a few very important states you’ll need to be aware of to be productive. A query can only be in one of the following states at any given moment:
isPendingorstatus === 'pending'- The query has no data yetisErrororstatus === 'error'- The query encountered an errorisSuccessorstatus === 'success'- The query was successful and data is available
Beyond those primary states, more information is available depending on the state of the query:
error- If the query is in anisErrorstate, the error is available via theerrorproperty.data- If the query is in anisSuccessstate, the data is available via thedataproperty.isFetching- In any state, if the query is fetching at any time (including background refetching)isFetchingwill be true.
For most queries, it’s usually sufficient to check for the isPending state, then the isError state, then finally, assume that the data is available and render the successful state:
function UserProfile({ userId }: { userId: string }) { const { isPending, isError, data, error } = useActorQuery({ functionName: "getUser", args: [userId], })
if (isPending) { return <span>Loading...</span> }
if (isError) { return <span>Error: {error.message}</span> }
// We can assume by this point that `isSuccess === true` return ( <div> <h2>{data?.name}</h2> <p>{data?.bio}</p> </div> )}If booleans aren’t your thing, you can always use the status state as well:
function UserProfile({ userId }: { userId: string }) { const { status, data, error } = useActorQuery({ functionName: "getUser", args: [userId], })
if (status === "pending") { return <span>Loading...</span> }
if (status === "error") { return <span>Error: {error.message}</span> }
// also status === 'success', but "else" logic works, too return ( <div> <h2>{data?.name}</h2> <p>{data?.bio}</p> </div> )}TypeScript will also narrow the type of data correctly if you’ve checked for pending and error before accessing it.
Conditional Queries
Section titled “Conditional Queries”You can use the enabled option to conditionally execute a query:
function UserProfile({ userId }: { userId: string | null }) { const { data } = useActorQuery({ functionName: "getUser", args: [userId!], // Only fetch when userId exists enabled: !!userId, })}This is particularly useful when:
- Waiting for other data before fetching
- Only fetching when authenticated
- Disabling queries in certain UI states
Selecting Data
Section titled “Selecting Data”Use the select option to transform or select a portion of the data:
const { data: userName } = useActorQuery({ functionName: "getUser", args: [userId], // Only return the name from the query select: (user) => user.name,})// data is now just the user's name (string)This is useful for:
- Extracting specific fields
- Transforming data for rendering
- Computing derived values
Stale Time
Section titled “Stale Time”Control when data is considered stale and should be refetched:
// Data is fresh for 5 minutesconst { data } = useActorQuery({ functionName: "getConfig", args: [], staleTime: 1000 * 60 * 5, // 5 minutes})
// Data is never stale (static data)const { data: metadata } = useActorQuery({ functionName: "getMetadata", args: [], staleTime: Infinity,})Refetching
Section titled “Refetching”Automatic Refetching
Section titled “Automatic Refetching”// Refetch every 10 secondsconst { data } = useActorQuery({ functionName: "getPrice", args: [], refetchInterval: 1000 * 10,})
// Refetch when window regains focusconst { data } = useActorQuery({ functionName: "getNotifications", args: [], refetchOnWindowFocus: true,})Manual Refetching
Section titled “Manual Refetching”const { data, refetch } = useActorQuery({ functionName: "getUser", args: [userId],})
// Trigger a refetchconst handleRefresh = () => { refetch()}Suspense Queries
Section titled “Suspense Queries”For Suspense-based data fetching, use useActorSuspenseQuery:
import { Suspense } from "react"import { useActorSuspenseQuery } from "../reactor/hooks"
function UserProfile({ userId }: { userId: string }) { // This hook suspends until data is loaded const { data } = useActorSuspenseQuery({ functionName: "getUser", args: [userId], })
// data is always defined here return ( <div> <h2>{data.name}</h2> <p>{data.bio}</p> </div> )}
// Wrap with Suspense boundaryfunction App() { return ( <Suspense fallback={<Loading />}> <UserProfile userId="user-123" /> </Suspense> )}Infinite Queries
Section titled “Infinite Queries”For paginated data, use useActorInfiniteQuery:
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } = useActorInfiniteQuery({ functionName: "getItems", getArgs: (pageParam) => [{ offset: pageParam, limit: 10 }], initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextOffset ?? undefined, })
return status === "pending" ? ( <p>Loading...</p>) : status === "error" ? ( <p>Error loading data</p>) : ( <> {data.pages.map((group, i) => ( <div key={i}> {group.items.map((item) => ( <p key={item.id}>{item.name}</p> ))} </div> ))} <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? "Loading more..." : hasNextPage ? "Load More" : "Nothing more to load"} </button> </>)Query Factories
Section titled “Query Factories”For reusable query configurations, use createQuery:
import { createQuery } from "@ic-reactor/react"import { backendActor } from "../reactor"
// Define query onceconst userQuery = createQuery(backendActor, { functionName: "getUser", args: ["user-123"], staleTime: 5 * 60 * 1000,})
// Use in componentsfunction UserProfile() { const { data, isLoading } = userQuery.useQuery() // ...}
// Use in loadersasync function loader() { const user = await userQuery.fetch() return { user }}
// Invalidate cacheawait userQuery.invalidate()For dynamic arguments, use createQueryFactory:
const getUserQuery = createQueryFactory(backendActor, { functionName: "getUser",})
// Create query with specific argsfunction UserProfile({ userId }: { userId: string }) { const userQuery = getUserQuery([userId]) const { data } = userQuery.useQuery() // ...}Type Safety
Section titled “Type Safety”IC Reactor provides full TypeScript support. The functionName prop autocompletes to valid canister methods, and args are type-checked:
// ✅ TypeScript knows this method exists and args are correctconst { data } = useActorQuery({ functionName: "getUser", // autocomplete available args: ["user-123"], // type checked})
// ❌ TypeScript error: wrong argument typeconst { data } = useActorQuery({ functionName: "getUser", args: [123], // Error: Expected string})Further Reading
Section titled “Further Reading”- Query Caching - Deep dive into TanStack Query integration
- Type Safety - Understanding the type system
- Error Handling - Handling query errors