Guides
Background fetching indicators
Show that a query is refreshing with isFetching. Prefer the router's pending UI for navigations; useIsFetching stays for compatibility when you need a global count.
A query's first load suspends, so Suspense fallbacks cover it. Later requests (refetches on focus, invalidation) happen in the background, while the old data stays on screen. Read isFetching on that query's controls. For navigations, prefer the router's pending UI — you usually don't need a global fetch counter.
One query: isFetching
isFetching on the reader's controls is true while any request for the query is in flight, first loads included:
function function Post(props: {
handle: QueryHandleWithData<typeof postQuery>;
}): React.JSX.Element
Post(props: {
handle: QueryHandleWithData<typeof postQuery>;
}
props: { handle: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
handle: interface QueryHandleWithData<TFactory extends QueryFactory = (input: unknown) => FactoryResult>QueryHandleWithData<typeof function postQuery(input: {
slug: string;
}): QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>
postQuery> }) {
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, props: {
handle: QueryHandleWithData<typeof postQuery>;
}
props.handle: QueryHandleWithData<(input: {
slug: string;
}) => QueryOptions<Flat<{
data: Post;
key: readonly ["post", string];
}>>>
handle);
return (
<React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article React.AriaAttributes["aria-busy"]?: Booleanish | undefinedaria-busy={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.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>
{const post: Postpost.Post.title: stringtitle}
{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 ? <function Spinner(props: {
readonly label: string;
}): React.ReactNode
Spinner label: stringlabel="Refreshing" /> : null}
</React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>
<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>
</React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article>
);
}
Controls are tracked: only components that read isFetching re-render when it changes. A parent that just passes the promise down stays put during background refetches.
Navigations
A navigation is not a background fetch: it's a transition. Use the router's pending state for the navigation itself:
- TanStack Start:
pendingComponent/pendingMs, oruseRouterState({ select: (state) => state.isLoading }) - Next.js:
useLinkStatusfromnext/link
isFetching covers what happens after the page has rendered.
useIsFetching
You probably don't need this. Prefer the router's pending UI for navigations (TanStack Start, Next.js) and per-query isFetching for background refetches. The hook stays for TanStack Query compatibility when you want one count of every in-flight request across the app:
function function GlobalProgress(): React.JSX.Element | nullGlobalProgress() {
const const fetching: numberfetching = function useIsFetching(filter?: Filter): numberuseIsFetching();
return const fetching: numberfetching > 0 ? <function ProgressBar(): React.ReactNodeProgressBar /> : null;
}Pass a filter to count only some queries:
const const savingComments: numbersavingComments = function useIsFetching(filter?: Filter): numberuseIsFetching({ Filter.queryKey?: QueryKey | undefinedqueryKey: ["comments"] });It's always 0 on the server and during hydration, so the indicator never appears in server HTML. Outside React, qc.isFetching(filter?) returns the same number.
Refetch the queries below, together or one at a time, and watch both indicators:
Avoiding flashes
On a fast connection, a background refetch can finish in 50 ms, and a spinner that flashes for that long is more distracting than helpful. Delay it with CSS, so it only appears for slower requests:
.spinner {
animation: fade-in 0s 300ms both;
}
@keyframes fade-in {
from {
opacity: 0;
}
}