Skip to content
IC Reactor

createSuspenseQuery

createSuspenseQuery creates a reusable query object that uses React Suspense for loading states. Data is always defined — no need for undefined checks.

import {
createSuspenseQuery,
createSuspenseQueryFactory,
} from "@ic-reactor/react"
import { createSuspenseQuery } from "@ic-reactor/react"
import { backend } from "./reactor"
const userQuery = createSuspenseQuery(backend, {
functionName: "get_user",
args: [userId],
})
Feature createQuery createSuspenseQuery
Loading handling Manual (isLoading) React Suspense
data type T | undefined T (always defined)
enabled option ✅ Supported ❌ Not available
Error handling error property Error Boundary
Render blocking No Yes (suspends)
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
Property Type Description
fetch () => Promise<T> Cache-first fetch — use in route loaders
prefetch () => Promise<void> Fire-and-forget cache warm-up
useSuspenseQuery (options?) => UseSuspenseQueryResult Suspense hook
invalidate () => Promise<void> Invalidate cache (refetches if active)
getQueryKey () => QueryKey Get the 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

A sign-in or sign-out while fetch() or prefetch() is in flight cancels its fetch, so the previous identity’s answer is never cached, and it runs again for the new identity. fetch() resolves with that answer rather than rejecting with TanStack’s CancelledError. prefetch() still never rejects, and when that run succeeds the cache holds the new identity’s answer by the time it resolves.


Combine loaders with Suspense for optimal UX:

// routes/users/$userId.tsx
import { createFileRoute } from "@tanstack/react-router"
import { Suspense } from "react"
import { createSuspenseQueryFactory } from "@ic-reactor/react"
import { backend } from "../../reactor"
const getUserQuery = createSuspenseQueryFactory(backend, {
functionName: "get_user",
})
export const Route = createFileRoute("/users/$userId")({
// Prefetch in loader
loader: async ({ params }) => {
await getUserQuery([params.userId]).fetch()
// No need to return data - it's in the cache
},
component: UserPageWrapper,
})
function UserPageWrapper() {
return (
<Suspense fallback={<UserSkeleton />}>
<UserPage />
</Suspense>
)
}
function UserPage() {
const { userId } = Route.useParams()
// data is NEVER undefined - Suspense handles loading
const { data: user } = getUserQuery([userId]).useSuspenseQuery()
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}

Use multiple Suspense boundaries for granular loading states:

import { Suspense } from "react"
import { createSuspenseQuery } from "@ic-reactor/react"
import { backend } from "./reactor"
const userProfileQuery = createSuspenseQuery(backend, {
functionName: "get_my_profile",
})
const dashboardStatsQuery = createSuspenseQuery(backend, {
functionName: "get_dashboard_stats",
})
const recentActivityQuery = createSuspenseQuery(backend, {
functionName: "get_recent_activity",
})
function Dashboard() {
return (
<div className="dashboard">
<Suspense fallback={<HeaderSkeleton />}>
<DashboardHeader />
</Suspense>
<div className="dashboard-content">
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity />
</Suspense>
</div>
</div>
)
}
function DashboardHeader() {
const { data: user } = userProfileQuery.useSuspenseQuery()
return <Header user={user} />
}
function StatsPanel() {
const { data: stats } = dashboardStatsQuery.useSuspenseQuery()
return <Stats data={stats} />
}
function RecentActivity() {
const { data: activity } = recentActivityQuery.useSuspenseQuery()
return <ActivityList items={activity} />
}

Handle errors with React Error Boundaries:

import { Suspense } from "react"
import { ErrorBoundary } from "react-error-boundary"
function UserPageWrapper() {
return (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div className="error">
<h2>Something went wrong</h2>
<pre>{error instanceof Error ? error.message : String(error)}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)}
>
<Suspense fallback={<UserSkeleton />}>
<UserPage />
</Suspense>
</ErrorBoundary>
)
}

Suspense queries in the same component run one after the other: the first one suspends the component before the next is reached, a waterfall. Give each query its own component and they start together, under one boundary:

import { Suspense } from "react"
import { createSuspenseQueryFactory } from "@ic-reactor/react"
import { backend } from "./reactor"
const getUserProfileQuery = createSuspenseQueryFactory(backend, {
functionName: "get_profile",
})
const getUserPostsQuery = createSuspenseQueryFactory(backend, {
functionName: "get_user_posts",
})
const getUserFollowersQuery = createSuspenseQueryFactory(backend, {
functionName: "get_user_followers",
})
function UserDashboard({ userId }: { userId: string }) {
// Sibling components suspend separately, so all three calls start at once
return (
<div>
<Profile userId={userId} />
<Posts userId={userId} />
<Followers userId={userId} />
</div>
)
}
function Profile({ userId }: { userId: string }) {
const { data: profile } = getUserProfileQuery([userId]).useSuspenseQuery()
return <ProfileCard profile={profile} />
}
function Posts({ userId }: { userId: string }) {
const { data: posts } = getUserPostsQuery([userId]).useSuspenseQuery()
return <PostsList posts={posts} />
}
function Followers({ userId }: { userId: string }) {
const { data: followers } = getUserFollowersQuery([userId]).useSuspenseQuery()
return <FollowersList followers={followers} />
}
// Wrap in single Suspense boundary
function UserDashboardPage() {
return (
<Suspense fallback={<DashboardSkeleton />}>
<UserDashboard userId={userId} />
</Suspense>
)
}

Prefetch data during route transitions. Use prefetch() (fire-and-forget) so you don’t need to await before navigating:

import { Link, useNavigate } from "@tanstack/react-router"
import { createSuspenseQueryFactory } from "@ic-reactor/react"
import { backend } from "./reactor"
const getUserQuery = createSuspenseQueryFactory(backend, {
functionName: "get_user",
})
function UserCard({ userId }: { userId: string }) {
const navigate = useNavigate()
const handleClick = () => {
// Fire-and-forget: warm cache, then navigate immediately
getUserQuery([userId]).prefetch()
navigate({ to: `/users/${userId}` })
}
return (
<div onClick={handleClick} className="user-card">
<Avatar userId={userId} />
<span>View Profile</span>
</div>
)
}
import { createSuspenseQuery } from "@ic-reactor/react"
import { backend } from "./reactor"
const userAvatarQuery = createSuspenseQuery(backend, {
functionName: "get_user",
args: [userId],
select: (user) => ({
url: user.avatarUrl,
alt: user.name,
}),
})
function Avatar() {
// data is { url, alt } - never undefined
const { data } = userAvatarQuery.useSuspenseQuery()
return <img src={data.url} alt={data.alt} />
}

For dynamic arguments:

import { createSuspenseQueryFactory } from "@ic-reactor/react"
import { backend } from "./reactor"
const getUserQuery = createSuspenseQueryFactory(backend, {
functionName: "get_user",
})
function UserProfile({ userId }: { userId: string }) {
// data is ALWAYS defined — Suspense handles loading
const { data: user } = getUserQuery([userId]).useSuspenseQuery()
return <h1>{user.name}</h1>
}

The factory function also has getQueryKey(), the key prefix every query it returns shares, and invalidate(), which invalidates all of them whatever the args. Pass the factory to a mutation’s invalidateQueries to refresh every instance:

import { createMutation, createSuspenseQueryFactory } from "@ic-reactor/react"
import { backend } from "./reactor"
const getUserQuery = createSuspenseQueryFactory(backend, {
functionName: "get_user",
})
const renameUser = createMutation(backend, {
functionName: "rename_user",
invalidateQueries: [getUserQuery],
})

See createQueryFactory for details.


✅ Use createSuspenseQuery when:

  • You want simpler component code (no isLoading checks)
  • Data must be available before rendering
  • You’re using React 18+ concurrent features
  • Your app already uses Suspense patterns

❌ Use createQuery when:

  • You need the enabled option for conditional fetching
  • You want to show partial UI while loading
  • You need fine-grained control over loading states
  • You’re migrating from non-Suspense patterns