Guides
Reading queries
useQuery returns a stable promise and tracked controls; unwrap the promise with use() under Suspense, or pass it down to a child.
Components read queries with useQuery, passing the factory and the handle the loader returned:
{
const [const promise: Thenable<Post>promise, const controls: QueryControls<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
controls] = useQuery<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>, undefined>(factory: (input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>, handle: QueryHandle<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>, options?: (Options & {
readonly throwOnError?: never;
} & {
readonly throwOnError?: undefined;
}) | undefined): readonly [promise: Thenable<Post>, controls: QueryControls<...>] (+1 overload)
useQuery(const factory: (input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>
factory, const handle: QueryHandle<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
handle, const options: Options | undefinedoptions);
}
{
const [const data: Postdata, const controls: QueryControls<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
controls] = useQuery<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>, undefined>(factory: (input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>, handle: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>, options?: (Options & {
readonly throwOnError?: never;
} & {
readonly throwOnError?: undefined;
}) | undefined): readonly [data: Post, controls: QueryControls<...>] (+1 overload)
useQuery(const factory: (input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>
factory, const handleWithData: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
handleWithData, const options: Options | undefinedoptions);
}promise is a stable Thenable<Data> owned by the cache: a Promise that also carries its status, value and reason. Unwrap it with use(). When the cache already has data, the promise is pre-resolved and use() returns synchronously without suspending.
A handle from ensure or fetch had its data loaded before the route rendered, so useQuery returns the data itself. See Handles for typing the props that receive one.
function function PostPage(): React.JSX.ElementPostPage() {
const const loaderData: {
comments: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<Flat<{
key: readonly ["comments", string];
page: CommentPage;
pageParam: number;
}>>>;
post: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>;
}
loaderData = const Route: Route<Register, Route<Register, RootRoute<Register, undefined, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", "/posts", "/posts", "/posts", ZodObject<{
sort: ZodDefault<ZodEnum<{
newest: "newest";
oldest: "oldest";
}>>;
page: ZodDefault<ZodInt>;
}, $strip>, ResolveParams<"/posts">, AnyContext, AnyContext, ... 7 more ..., undefined>, ... 15 more ..., undefined>
Route.RouteExtensions<"/posts/$slug", "/posts/$slug">.useLoaderData: <RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>, unknown, boolean>(opts?: (UseLoaderDataBaseOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, ... 8 more ..., undefined>, "never", false, RouterHistory, Record<...>>, "/posts/$slug", true, unknown, boolean> & OptionalStructuralSharing<...>) | undefined) => {
...;
}
useLoaderData();
// `post` came from `qc.ensure`, `comments` from `qc.prefetch`.
const [const post: Postpost, const postState: QueryControls<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
postState] = useQuery<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>, undefined>(factory: (input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>, handle: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>, options?: (Options & {
readonly throwOnError?: never;
} & {
readonly throwOnError?: undefined;
}) | undefined): readonly [data: Post, controls: QueryControls<...>] (+1 overload)
useQuery(function postQuery(input: {
slug: string;
}): QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>
postQuery, const loaderData: {
comments: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<Flat<{
key: readonly ["comments", string];
page: CommentPage;
pageParam: number;
}>>>;
post: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>;
}
loaderData.post: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
post);
return (
<React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article>
<React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const post: Postpost.Post.title: stringtitle}</React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>
<React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button
React.ButtonHTMLAttributes<HTMLButtonElement>.disabled?: boolean | undefineddisabled={const postState: QueryControls<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
postState.QueryControls<(input: { slug: string; }) => QueryOptions<Flat<{ data: Post; key: readonly ["post", string]; }>>>.isFetching: booleanisFetching}
React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() => function startTransition(scope: React.TransitionFunction): voidstartTransition(() => const postState: QueryControls<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
postState.QueryControls<(input: { slug: string; }) => QueryOptions<Flat<{ data: Post; key: readonly ["post", string]; }>>>.refetch(): Promise<void>refetch())}
>
Refresh
</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>
<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>{const post: Postpost.Post.body: stringbody}</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>
<const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: React.ReactNodefallback={<function CommentsSkeleton(): React.ReactNodeCommentsSkeleton />}>
<function Comments(props: {
handle: QueryHandle<typeof commentsQuery>;
}): React.ReactNode
Comments handle: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<Flat<{
key: readonly ["comments", string];
page: CommentPage;
pageParam: number;
}>>>
handle={const loaderData: {
comments: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<Flat<{
key: readonly ["comments", string];
page: CommentPage;
pageParam: number;
}>>>;
post: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>;
}
loaderData.comments: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<Flat<{
key: readonly ["comments", string];
page: CommentPage;
pageParam: number;
}>>>
comments} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense>
</React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article>
);
}
options takes whilePending and per-read refetch settings (refetchInterval, refetchOnWindowFocus, refetchOnReconnect). There is no option to change the input: the handle decides what is read.
Controls
| Field | Meaning |
|---|---|
isPending | No data yet for the handle's input; use(promise) will suspend |
isPrevious | The promise holds data for a previous input while the handle's input loads (whilePending: "previous") |
isFetching | A request is in flight: first load, new input, or background refetch |
isStale | Data is older than staleTime, or was invalidated |
error | Error from a failed background refetch. Existing data stays readable |
updatedAt | When the data in the promise was fetched |
key, input | Key and input of the data in the promise. They differ from the handle's while isPrevious is true |
hash | Stable string form of the handle's key, for key props on Suspense boundaries |
refetch() | Refetch now. Returns a promise, so it can be awaited inside a transition or action |
Paged factories add fetchNextPage(), fetchPreviousPage(), hasNextPage, hasPreviousPage, isFetchingNextPage and isFetchingPreviousPage. See Infinite queries.
Updates never suspend
When the cache updates — refetch, invalidation, setData — the hook returns a new, pre-resolved promise and the component re-renders without suspending. New data is structurally shared with the old: a refetch that returns equal data keeps the same promise, so nothing re-renders. See Render optimizations.
Errors
When the first load fails, the promise rejects and use() throws to the nearest error boundary. Resetting that boundary refetches the query. A failed background refetch doesn't throw: the old data stays on screen and controls.error holds the error, so the component can show it next to the data. Retries has a demo of both.
Effect queryFns can return a typed Result in the first tuple slot instead of rejecting — see Typed errors.
Passing promises down
A component can call the hook with a prefetched handle and hand the promise to a child, so only the child suspends:
const [const postPromise: Thenable<Post>postPromise] = useQuery<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>, undefined>(factory: (input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>, handle: QueryHandle<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>, options?: (Options & {
readonly throwOnError?: never;
} & {
readonly throwOnError?: undefined;
}) | undefined): readonly [promise: Thenable<Post>, controls: QueryControls<...>] (+1 overload)
useQuery(function postQuery(input: {
slug: string;
}): QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>
postQuery, const loaderData: {
post: QueryHandle<typeof postQuery>;
}
loaderData.post: QueryHandle<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
post); // from `qc.prefetch`
return (
<const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: ReactNodefallback={<function Spinner(): ReactNodeSpinner />}>
<function PostBody(props: {
post: Promise<Post>;
}): React.JSX.Element
PostBody post: Promise<Post>post={const postPromise: Thenable<Post>postPromise} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense>
);
A component re-renders when the promise changes, and when a control it reads changes. Controls it never reads, like isFetching here, don't re-render it, so a parent that only passes the promise down stays put during background refetches.
Because use() can be called conditionally, a component can also decide whether to read:
function function Details(props: {
open: boolean;
details: Promise<Details>;
}): React.JSX.Element | null
Details(props: {
open: boolean;
details: Promise<Details>;
}
props: { open: booleanopen: boolean; details: Promise<Details>details: interface Promise<T>Promise<Details> }) {
if (!props: {
open: boolean;
details: Promise<Details>;
}
props.open: booleanopen) return null;
const const details: Detailsdetails = use<Details>(usable: React.Usable<Details>): Detailsuse(props: {
open: boolean;
details: Promise<Details>;
}
props.details: Promise<Details>details);
return <function DetailsTable(props: {
details: Details;
}): React.ReactNode
DetailsTable details: Detailsdetails={const details: Detailsdetails} />;
}No refetch on mount
useQuery never refetches on mount: whatever started the query decides. Stale data refetches when a loader or server component starts the query again, when useWaterfall mounts, when invalidated, on window focus or reconnect, on refetch(), or on refetchInterval.
That means rendering the same handle in many components is free: they share one entry and one request. See Background fetching indicators to show when a refetch is running.