Guides
Starting queries
load with prefetch, swr and revalidate: what each waits for, what it returns, and how to choose between granular skeletons and a blocked navigation.
Queries start where the page's inputs are known: a route loader, a server component, or an event handler. load takes the client and a record of entries, one per query. Each entry names a factory and its input, and its kind decides what the load waits for:
| Entry | Cache miss | Stale data | TanStack Query |
|---|---|---|---|
prefetch(factory, input) | Starts the request | Refetches in the background | prefetchQuery, not awaited |
swr(factory, input) | Waits for the request | Resolves at once, refetches in the background | ensureQueryData({ revalidateIfStale: true }) |
revalidate(factory, input) | Waits for the request | Waits for the refetch | fetchQuery |
Fresh cached data is reused by all three, without a request. The names come from HTTP's Cache-Control directives, with staleTime as the max-age: swr is stale-while-revalidate, which serves stale data while it refetches, and revalidate is must-revalidate, which refetches stale data before using it.
So prefetch never holds anything up, swr holds up only the first visit (returning to a page shows the cached data at once, then fresh data), and revalidate always returns fresh data, at the cost of a round trip whenever the cache is stale. That difference needs a client that keeps its cache between navigations, as a TanStack Router loader's or a client-only React Router clientLoader's does. Code that only runs on the server has none: see Server-only code.
load resolves to [handles, data]: a QueryHandle for every entry, and the data of the swr and revalidate ones. A component reads every handle the same way, and use() returns a waited-for query's data without suspending. See Handles.
export const 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 = createFileRoute<"/posts/$slug", 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>, "/posts/$slug", "/$slug", "/posts/$slug">(path?: "/posts/$slug" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>
Creates a file-based Route factory for a given path.
createFileRoute("/posts/$slug")({
FilebaseRouteOptionsInterface<Register, Route<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 15 more ..., undefined>, ... 12 more ..., undefined>.loader?: Constrain<(args: LoaderFnContext<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, ... 8 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, RouteLoaderFn<...> | RouteLoaderObject<...>> | undefined
loader: async (args: LoaderFnContext<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>, ... 7 more ..., undefined>
args) => {
const { const slug: stringslug } = args: LoaderFnContext<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>, ... 7 more ..., undefined>
args.LoaderFnContext<Register, Route<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 15 more ..., undefined>, ... 7 more ..., undefined>.params: {
slug: string;
}
params;
const [const handles: {
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>>;
}
handles] = await load<{
readonly post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "swr">;
readonly comments: LoadEntry<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
}>(client: QueryClient, entries: {
readonly post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "swr">;
readonly comments: LoadEntry<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
}): Promise<...>
Start a loader's queries and return a serializable handle for each. It
starts every entry before it waits for any, so the order of the entries
can't create a waterfall. Resolves with [handles, data] once every
swr and revalidate entry has data, and rejects with the error as soon
as one fails.
prefetch entries never make it reject. A skipToken in place of an
entry starts nothing and gets no handle.
load(args: LoaderFnContext<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>, ... 7 more ..., undefined>
args.LoaderFnContext<Register, Route<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 15 more ..., undefined>, ... 7 more ..., undefined>.context: {
queryClient: QueryClient;
}
context.queryClient: QueryClient<any>queryClient, {
// Waits only if the post isn't in the cache yet.
post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "swr">
post: swr<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>(factory: (input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, ...args: LoadEntryArgs<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>): LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "swr">
Use cached data even when it's stale, and wait only when the cache has
none. Stale data resolves at once and refetches in the background, as
HTTP's
stale-while-revalidate
does. The load rejects if the fetch it waits for fails.
swr(function postQuery(input: {
slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery, { slug: stringslug }),
// Never waits: the comments load while the page renders.
comments: LoadEntry<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">
comments: prefetch<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>(factory: (input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, ...args: LoadEntryArgs<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>): LoadEntry<...>
Start the query if the cache has no fresh data, and don't wait for it.
It never makes the load reject: errors show up where a component reads
the handle.
prefetch(function commentsQuery(input: {
slug: string;
}): InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery, { slug: stringslug }),
});
return const handles: {
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>>;
}
handles;
},
UpdatableRouteOptionsExtensions.component?: RouteComponent | undefinedcomponent: function PostPage(): React.JSX.ElementPostPage,
});
prefetch or swr?
Neither is faster: both start the request at the same moment. The difference is what the user sees while it loads:
prefetchnavigates now. The new page renders at once, and each part still waiting for data shows its nearest Suspense fallback.swrnavigates when the data is in. Until then the previous page stays on screen. In TanStack Router, a route'spendingComponentreplaces it afterpendingMs(1 second by default).
Choose per query: swr for what the page can't lay out without (the post itself), and prefetch for the rest (comments, sidebars, counts).
Awaiting several queries
Several waiting entries in one load wait together, and data has each one's data, typed by its factory. The load rejects as soon as one of them fails:
const [const handles: {
readonly post: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
readonly author: QueryHandle<(input: {
id: string;
}) => QueryOptions<readonly ["authors", string], Author, Author, DefaultTypes>>;
}
handles, const data: LoadedData<{
readonly post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "swr">;
readonly author: LoadEntry<(input: {
id: string;
}) => QueryOptions<readonly ["authors", string], Author, Author, DefaultTypes>, "swr">;
}>
data] = await load<{
readonly post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "swr">;
readonly author: LoadEntry<(input: {
id: string;
}) => QueryOptions<readonly ["authors", string], Author, Author, DefaultTypes>, "swr">;
}>(client: QueryClient, entries: {
readonly post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "swr">;
readonly author: LoadEntry<(input: {
id: string;
}) => QueryOptions<readonly ["authors", string], Author, Author, DefaultTypes>, "swr">;
}): Promise<...>
Start a loader's queries and return a serializable handle for each. It
starts every entry before it waits for any, so the order of the entries
can't create a waterfall. Resolves with [handles, data] once every
swr and revalidate entry has data, and rejects with the error as soon
as one fails.
prefetch entries never make it reject. A skipToken in place of an
entry starts nothing and gets no handle.
load(const qc: QueryClient<any>qc, {
post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "swr">
post: swr<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>(factory: (input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, ...args: LoadEntryArgs<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>): LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "swr">
Use cached data even when it's stale, and wait only when the cache has
none. Stale data resolves at once and refetches in the background, as
HTTP's
stale-while-revalidate
does. The load rejects if the fetch it waits for fails.
swr(function postQuery(input: {
slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery, { slug: stringslug }),
author: LoadEntry<(input: {
id: string;
}) => QueryOptions<readonly ["authors", string], Author, Author, DefaultTypes>, "swr">
author: swr<(input: {
id: string;
}) => QueryOptions<readonly ["authors", string], Author, Author, DefaultTypes>>(factory: (input: {
id: string;
}) => QueryOptions<readonly ["authors", string], Author, Author, DefaultTypes>, ...args: LoadEntryArgs<(input: {
id: string;
}) => QueryOptions<readonly ["authors", string], Author, Author, DefaultTypes>>): LoadEntry<(input: {
id: string;
}) => QueryOptions<readonly ["authors", string], Author, Author, DefaultTypes>, "swr">
Use cached data even when it's stale, and wait only when the cache has
none. Stale data resolves at once and refetches in the background, as
HTTP's
stale-while-revalidate
does. The load rejects if the fetch it waits for fails.
swr(function authorQuery(input: {
id: string;
}): QueryOptions<readonly ["authors", string], Author, Author, DefaultTypes>
authorQuery, { id: stringid: const slug: stringslug }),
});
const data: LoadedData<{
readonly post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "swr">;
readonly author: LoadEntry<(input: {
id: string;
}) => QueryOptions<readonly ["authors", string], Author, Author, DefaultTypes>, "swr">;
}>
data.author.Author.name: stringname;return const handles: {
readonly post: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
readonly author: QueryHandle<(input: {
id: string;
}) => QueryOptions<readonly ["authors", string], Author, Author, DefaultTypes>>;
}
handles;
Conditional queries
Some queries only start for some requests, such as drafts that only an admin sees. Put skipToken in place of the entry when the query shouldn't start:
async function function loader(slug: string): Promise<{
readonly post: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
readonly drafts: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>> | undefined;
}>
loader(slug: stringslug: string) {
const [const handles: {
readonly post: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
readonly drafts: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>> | undefined;
}
handles] = await load<{
readonly post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "swr">;
readonly drafts: typeof skipToken | LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>, "prefetch">;
}>(client: QueryClient, entries: {
readonly post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "swr">;
readonly drafts: typeof skipToken | LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>, "prefetch">;
}): Promise<...>
Start a loader's queries and return a serializable handle for each. It
starts every entry before it waits for any, so the order of the entries
can't create a waterfall. Resolves with [handles, data] once every
swr and revalidate entry has data, and rejects with the error as soon
as one fails.
prefetch entries never make it reject. A skipToken in place of an
entry starts nothing and gets no handle.
load(const queryClient: QueryClient<any>queryClient, {
post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "swr">
post: swr<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>(factory: (input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, ...args: LoadEntryArgs<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>): LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "swr">
Use cached data even when it's stale, and wait only when the cache has
none. Stale data resolves at once and refetches in the background, as
HTTP's
stale-while-revalidate
does. The load rejects if the fetch it waits for fails.
swr(function postQuery(input: {
slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery, { slug: stringslug }),
// Only admins see drafts: for everyone else, nothing starts.
drafts: typeof skipToken | LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>, "prefetch">
drafts: const user: {
readonly isAdmin: boolean;
}
user.isAdmin: booleanisAdmin ? prefetch<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>>(factory: (input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>, ...args: LoadEntryArgs<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>>): LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>, "prefetch">
Start the query if the cache has no fresh data, and don't wait for it.
It never makes the load reject: errors show up where a component reads
the handle.
prefetch(function draftsQuery(input: {
slug: string;
}): QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>
draftsQuery, { slug: stringslug }) : const skipToken: typeof skipTokenIn place of a load entry: skip the query. Its handle is undefined and
data has no key for it, so a component renders its reader only when
there's a handle.
skipToken,
});
const handles: {
readonly post: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
readonly drafts: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>> | undefined;
}
handles.drafts; return const handles: {
readonly post: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
readonly drafts: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>> | undefined;
}
handles;
}
A skipToken starts nothing. Its handle is undefined, so the handle's type is QueryHandle<typeof draftsQuery> | undefined, and data has no key for it. useQuery takes only a handle, so render the component that reads the query only when there is one:
function function PostSidebar(): React.JSX.Element | nullPostSidebar() {
const const draftsHandle: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>> | undefined
draftsHandle = const loaderData: {
readonly post: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>;
readonly drafts: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>> | undefined;
}
loaderData.drafts: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>> | undefined
drafts;
if (!const draftsHandle: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>> | undefined
draftsHandle) {
return null;
}
return (
<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={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading drafts…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}>
<function Drafts(props: {
handle: QueryHandle<typeof draftsQuery>;
}): React.JSX.Element
Drafts handle: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>>
handle={const draftsHandle: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>>
draftsHandle} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Lets you display a fallback until its children have finished loading.
Suspense>
);
}
function function Drafts(props: {
handle: QueryHandle<typeof draftsQuery>;
}): React.JSX.Element
Drafts(props: {
handle: QueryHandle<typeof draftsQuery>;
}
props: { handle: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>>
handle: interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => { readonly queryFn: (context: never) => unknown; readonly queryKey: QueryKey; }>A small serializable reference to a query: its key and its factory's
input. It carries no data; readers get that from the cache. Handles of
different factories have different types.
QueryHandle<typeof function draftsQuery(input: {
slug: string;
}): QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>
draftsQuery> }) {
const [const draftsThenable: QueryValue<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>>
draftsThenable] = useQuery<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>>(factory: (input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>, handle: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>>, options?: UseQueryOptions): readonly [QueryValue<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>>, QueryState<...>]
Read a query by its handle.
useQuery(function draftsQuery(input: {
slug: string;
}): QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>
draftsQuery, props: {
handle: QueryHandle<typeof draftsQuery>;
}
props.handle: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>>
handle);
return <function DraftList(props: {
readonly drafts: ReadonlyArray<Draft>;
}): React.ReactNode
DraftList drafts: readonly Draft[]drafts={use<readonly Draft[]>(usable: React.Usable<readonly Draft[]>): readonly Draft[]use(const draftsThenable: QueryValue<(input: {
slug: string;
}) => QueryOptions<readonly ["drafts", string], readonly Draft[], readonly Draft[], DefaultTypes>>
draftsThenable)} />;
}
The condition is decided where the loader runs, from what it knows: the route's params and search, its context, or another entry's data that it waited for. Data only the browser has, such as a permission, has no loader entry: see Browser-only queries.
When to use revalidate
revalidate is swr that also waits on stale data. Use it when the code that starts the query needs the data to be fresh, not just present:
- a loader that must branch on the data, such as a redirect
- outside React, in scripts and tests
In a browser loader, prefer swr: a stale revalidate blocks the navigation on the refetch, while a stale swr renders the cached data at once and updates when the refetch lands. TanStack Router hides the difference while it still holds the route's last loader data: it renders that and reruns the loader in the background, unless the loader's staleReloadMode is "blocking", as in the demo. There, open a post, go back, and reopen it once its cache is stale.
Server-only code
React server components and React Router's loader run only on the server, for every navigation, and each request gets a new client. Its cache starts empty, so every query is a cache miss, so swr and revalidate wait for the same request every time, and returning to a page waits again.
Use prefetch and revalidate in that code, not swr: prefetch for what can stream in behind a Suspense fallback, revalidate for what the page can't lay out without or the code needs to branch on. See React Server Components and React Router.
A client-only React Router app is different: its clientLoaders run in the browser on the tab's client, which keeps its cache between navigations, so there swr holds up only the first visit, as in TanStack Router. See React Router.
Behavior
- Every entry dedupes against the cache: a request already in flight for the entry is joined, not repeated.
- Loaders run on every navigation (TanStack Router reruns them when its own
staleTimehas passed,0by default), so returning to a page shows what's cached at once and then fresh data. WithdefaultPreload: "intent", also setdefaultPreloadStaleTime: 0: otherwise the router reuses a hover's preload for 30 seconds instead of running the loader on click, and the query cache never hears about the navigation. - On the server,
staleTimedoesn't apply: the client belongs to one request, so what it fetched is fresh for that request.prefetchandswrreuse cached data even after an invalidation.revalidatereuses it until an invalidation, then waits for the refetch. - A
skipTokenin place of an entry starts nothing and gets anundefinedhandle. See Conditional queries. - A failed
prefetchnever makes the load reject. The error surfaces where the data is read. - A failed
swrorrevalidatemakes the load reject, so it reaches the route's error boundary.
Preloading on intent
Outside loaders, call load yourself. Starting a query when the pointer enters a link means the click finds it cached or in flight:
<const Link: LinkComponent<"a", string>A strongly-typed anchor component for declarative navigation.
Handles path, search, hash and state updates with optional route preloading
and active-state styling.
Link
RequiredToOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, { queryClient: QueryClient; }, AnyContext, AnyContext, {}, undefined, ... 4 more ..., undefined>, "never", false, RouterHistory, Record<...>>, string, "/posts/$slug">.to: "." | ".." | "/posts/$slug" | "/" | "/login" | "/posts" | "/settings" | "/dashboard" | "/blog/$slug"The internal route path to navigate to. This should be a relative or absolute path within your application.
For external URLs, use the href property instead.
to="/posts/$slug"
MakeRequiredPathParams<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, { queryClient: QueryClient; }, AnyContext, AnyContext, {}, ... 5 more ..., undefined>, "never", false, RouterHistory, Record<...>>, string, "/posts/$slug">.params: {
slug: string;
} | ParamsReducerFn<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>, "PATH", string, "/posts/$slug">
params={{ slug: stringslug }}
onPointerEnter?: React.PointerEventHandler<HTMLAnchorElement> | undefinedonPointerEnter={() => {
void load<{
readonly post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "prefetch">;
}>(client: QueryClient, entries: {
readonly post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "prefetch">;
}): Promise<Loaded<{
readonly post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "prefetch">;
}>>
Start a loader's queries and return a serializable handle for each. It
starts every entry before it waits for any, so the order of the entries
can't create a waterfall. Resolves with [handles, data] once every
swr and revalidate entry has data, and rejects with the error as soon
as one fails.
prefetch entries never make it reject. A skipToken in place of an
entry starts nothing and gets no handle.
load(const qc: QueryClient<any>qc, { post: LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "prefetch">
post: prefetch<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>(factory: (input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, ...args: LoadEntryArgs<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>): LoadEntry<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, "prefetch">
Start the query if the cache has no fresh data, and don't wait for it.
It never makes the load reject: errors show up where a component reads
the handle.
prefetch(function postQuery(input: {
slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery, { slug: stringslug }) });
}}
>
{const title: stringtitle}
</const Link: LinkComponent<"a", string>A strongly-typed anchor component for declarative navigation.
Handles path, search, hash and state updates with optional route preloading
and active-state styling.
Link>;With TanStack Router's defaultPreload: "intent" you get this for free: hovering a link runs its loader, which starts its queries.
Paged factories take a third argument on swr and revalidate: { pages: n } loads the first n pages up front. See Infinite queries.