Guides
Reading queries
useQuery returns a stable thenable and the query's state; read it with use() under Suspense, switch on its status, or pass it down to a child.
Components read queries with useQuery, passing the factory and the handle the loader returned:
const [const postThenable: QueryValue<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable, const postState: QueryState<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postState] = useQuery<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>(factory: (input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, handle: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>, options?: UseQueryOptions): readonly [QueryValue<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>, QueryState<...>]
Read a query by its handle.
useQuery(const factory: (input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
factory, const handle: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
handle, const options: UseQueryOptions | undefinedoptions);
const const post: Postpost = use<Post>(usable: React.Usable<Post>): Postuse(const postThenable: QueryValue<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable);postThenable is a stable Thenable<Data> owned by the cache: a Promise that also carries status, value and reason, the fields React's use() reads. Read it with use() on its own line. When the cache already has the data, the thenable is fulfilled and use() returns it synchronously; otherwise use() suspends until it lands.
useQuery returns a thenable for every handle. A handle from swr or revalidate had its data loaded before the route rendered, so use() doesn't suspend on it. If the cache dropped the data since (garbage collection, resetOne, removeOne), use() suspends while the query refetches. See Handles.
function function PostPage(): React.JSX.ElementPostPage() {
const const loaderData: {
readonly post: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
readonly comments: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
}
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 `swr`, `comments` from `prefetch`.
const [const postThenable: QueryValue<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable, const postState: QueryState<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postState] = useQuery<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>(factory: (input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, handle: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>, options?: UseQueryOptions): readonly [...]
Read a query by its handle.
useQuery(function postQuery(input: {
slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery, const loaderData: {
readonly post: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
readonly comments: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
}
loaderData.post: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
post);
const const post: Postpost = use<Post>(usable: React.Usable<Post>): Postuse(const postThenable: QueryValue<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable);
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: QueryState<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postState.QueryState<(input: { slug: string; }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>.isFetching: booleanisFetching}
React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() => function startTransition(scope: React.TransitionFunction): voidSimilar to useTransition but allows uses where hooks are not available.
startTransition(() => const postState: QueryState<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postState.QueryState<(input: { slug: string; }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>.refetch(): Promise<void>Fetch again, or join the request in flight. Resolves when it settles.
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>Lets you display a fallback until its children have finished loading.
Suspense React.SuspenseProps.fallback?: React.ReactNodeA fallback react tree to show when a Suspense child (like React.lazy) suspends
fallback={<function CommentsSkeleton(): React.ReactNodeCommentsSkeleton />}>
<function Comments(props: {
handle: QueryHandle<typeof commentsQuery>;
}): React.ReactNode
Comments handle: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
handle={const loaderData: {
readonly post: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
readonly comments: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
}
loaderData.comments: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
comments} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Lets you display a fallback until its children have finished loading.
Suspense>
</React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article>
);
}
options takes whilePending. There is no option to change the input: the handle decides what is read. Settings such as polling belong to the query, as meta in its factory.
State
| Field | Meaning |
|---|---|
isPending | No data yet for the handle's input; use() on the thenable will suspend |
isPrevious | The thenable holds the previous handle's data while this handle's query 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 | The last request's error while it's the latest outcome |
updatedAt | When the handle's data was fetched; 0 without data |
queryKey, input | The handle's key and input |
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 read with useInfiniteQuery, whose state adds 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, fulfilled thenable and the component re-renders without suspending. New data is structurally shared with the old: a refetch that returns equal data keeps the same thenable and the same objects, so memoized children don't re-render. See Render optimizations.
Errors
When the first load fails, the thenable rejects and use() throws to the nearest error boundary. A failed background refetch doesn't throw: the old data stays on screen and the state's error holds the error, so the component can show it next to the data. Retries has a demo of both.
Before resetting the error boundary, call resetOne(qc, factory, input), so the query loads again instead of throwing the same error.
Passing thenables down
A component can call the hook with a prefetched handle and hand the thenable to a child, so only the child suspends:
const [const postThenable: QueryValue<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable] = useQuery<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>(factory: (input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, handle: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>, options?: UseQueryOptions): readonly [...]
Read a query by its handle.
useQuery(function postQuery(input: {
slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery, const loaderData: {
post: QueryHandle<typeof postQuery>;
}
loaderData.post: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
post); // from `prefetch`
return (
<const Suspense: React.ExoticComponent<React.SuspenseProps>Lets you display a fallback until its children have finished loading.
Suspense React.SuspenseProps.fallback?: ReactNodeA fallback react tree to show when a Suspense child (like React.lazy) suspends
fallback={<function Spinner(): ReactNodeSpinner />}>
<function PostBody(props: {
post: Promise<Post>;
}): React.JSX.Element
PostBody post: Promise<Post>post={const postThenable: QueryValue<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Lets you display a fallback until its children have finished loading.
Suspense>
);
The component that calls useQuery re-renders when its thenable changes, and when a state field it read changes, such as isFetching during a background refetch. A parent that should stay put can pass the handle down instead, so only the child reads the query. See Tracked state.
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>Represents the completion of an asynchronous operation
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 stale data on mount: whatever started the query decides. Stale data refetches when a loader or server component starts the query again, when it's invalidated (a reader that mounts after an invalidation refetches it), on refetch(), and with the client's features: window focus and reconnect refetching, and polling. A reader only fetches on its own when the query has no data, which is a waterfall.
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.