Skip to content

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.

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.

useSuspenseQ works in a streaming React Server Components setup. The pattern:

  1. Pre-fetch on the server using the raw fetcher.
  2. Hydrate the client cache.
  3. 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.

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 isFetching or isRefetching inside the component.
  • You want the component to render its own skeleton inline rather than bubbling to a boundary.
  • The data is optional/conditional (use useQ with enabled).

useSuspenseQ returns the same object as TanStack Query’s useSuspenseQuery: { data, error, isFetching, refetch, … }. The big differences vs. useQ:

  • data is the response type (not T | undefined).
  • There’s no isLoading — the suspense boundary handles it.
  • error is always null while the component is mounted (a real error throws up to the boundary).