createSuspenseQuery
createSuspenseQuery creates a reusable query object that uses React Suspense for loading states. Data is always defined — no need for undefined checks.
Import
Section titled “Import”import { createSuspenseQuery, createSuspenseQueryFactory,} from "@ic-reactor/react"Basic Usage
Section titled “Basic Usage”import { createSuspenseQuery } from "@ic-reactor/react"import { backend } from "./reactor"
const userQuery = createSuspenseQuery(backend, { functionName: "get_user", args: [userId],})Key Differences from createQuery
Section titled “Key Differences from createQuery”| 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) |
Configuration
Section titled “Configuration”| 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 |
Return Value
Section titled “Return Value”| 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.
Examples
Section titled “Examples”TanStack Router with Suspense
Section titled “TanStack Router with Suspense”Combine loaders with Suspense for optimal UX:
// routes/users/$userId.tsximport { 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> )}Nested Suspense Boundaries
Section titled “Nested Suspense Boundaries”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} />}Error Boundary Integration
Section titled “Error Boundary Integration”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> )}Parallel Suspense Queries
Section titled “Parallel Suspense Queries”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 boundaryfunction UserDashboardPage() { return ( <Suspense fallback={<DashboardSkeleton />}> <UserDashboard userId={userId} /> </Suspense> )}Prefetching on Route Transition
Section titled “Prefetching on Route Transition”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> )}With Select Transformation
Section titled “With Select Transformation”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} />}createSuspenseQueryFactory
Section titled “createSuspenseQueryFactory”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.
When to Use Suspense
Section titled “When to Use Suspense”✅ Use createSuspenseQuery when:
- You want simpler component code (no
isLoadingchecks) - 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
enabledoption 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
See Also
Section titled “See Also”- createQuery — Non-Suspense version
- createSuspenseInfiniteQuery — Suspense pagination
- useActorSuspenseQuery — Direct hook usage