useSuspenseQ
useSuspenseQ is the suspense counterpart of useQ. It throws a promise while loading and an ApiError on failure, so the loading/error UI lives in the boundary instead of the component:
function PostList({ facilityId }: { facilityId: string }) { const { data } = useSuspenseQ("listPosts", { pathParams: { facilityId }, }); return ( <ul> {data.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> );}Note data is non-nullable — by the time your component renders, the data has resolved.
With <Suspense> + <ApiErrorBoundary>
Section titled “With <Suspense> + <ApiErrorBoundary>”import { Suspense } from "react";import { ApiErrorBoundary, isApiError } from "@use-q/api-client-react";
function PostsPage({ facilityId }: { facilityId: string }) { return ( <ApiErrorBoundary fallback={(error, reset) => ( <div role="alert"> <p> {isApiError(error) ? `${error.status} — ${error.message}` : "Unknown error"} </p> <button onClick={reset}>Try again</button> </div> )} > <Suspense fallback={<p>Loading posts…</p>}> <PostList facilityId={facilityId} /> </Suspense> </ApiErrorBoundary> );}Order matters: the error boundary must wrap <Suspense>. If they swap, suspension itself counts as an error.
RSC-friendly pattern
Section titled “RSC-friendly pattern”useSuspenseQ works in a streaming React Server Components setup. The pattern:
- Pre-fetch on the server using the raw fetcher.
- Hydrate the client cache.
- Render the client component, which immediately suspends and resolves with the hydrated data.
// app/posts/page.tsx (RSC)import { dehydrate, HydrationBoundary } from "@tanstack/react-query";import { api } from "@/api/client";import { PostsPage } from "./PostsPage";
export default async function Page() { const queryClient = api.queryClient;
await queryClient.prefetchQuery({ queryKey: api.queryKeys.listPosts({ pathParams: { facilityId: "f1" } }), queryFn: ({ signal }) => api.fetcher.fetch("listPosts", { pathParams: { facilityId: "f1" }, signal, }), });
return ( <HydrationBoundary state={dehydrate(queryClient)}> <PostsPage facilityId="f1" /> </HydrationBoundary> );}// app/posts/PostsPage.tsx — client"use client";
import { Suspense } from "react";import { useSuspenseQ } from "@/api/client";import { ApiErrorBoundary } from "@use-q/api-client-react";
function PostList({ facilityId }: { facilityId: string }) { const { data } = useSuspenseQ("listPosts", { pathParams: { facilityId } }); return data.map((p) => <article key={p.id}>{p.title}</article>);}
export function PostsPage({ facilityId }: { facilityId: string }) { return ( <ApiErrorBoundary fallback={() => <p>Couldn't load.</p>}> <Suspense fallback={<p>Loading…</p>}> <PostList facilityId={facilityId} /> </Suspense> </ApiErrorBoundary> );}See SSR & loaders for the full hydration story.
When to prefer useQ instead
Section titled “When to prefer useQ instead”useSuspenseQ is great when:
- You’re already using suspense boundaries for code-splitting.
- The component would otherwise be a tangle of
if (isLoading) return …; if (error) return …;branches.
It’s the wrong fit when:
- You need fine-grained access to
isFetchingorisRefetchinginside the component. - You want the component to render its own skeleton inline rather than bubbling to a boundary.
- The data is optional/conditional (use
useQwithenabled).
Returned shape
Section titled “Returned shape”useSuspenseQ returns the same object as TanStack Query’s useSuspenseQuery: { data, error, isFetching, refetch, … }. The big differences vs. useQ:
datais the response type (notT | undefined).- There’s no
isLoading— the suspense boundary handles it. erroris alwaysnullwhile the component is mounted (a real error throws up to the boundary).