Integrations
TanStack Start
Set up raf-query with TanStack Router and Start, start queries in loaders, stream them during SSR and handle redirects.
TanStack Router's loaders are where raf-query wants queries to start, and its SSR streaming is what carries the cache to the browser. The integration is one function call.
Setup
import { function createQueryClient<const TLinks extends ReadonlyArray<AnyLink> = [], const TFeatures extends ReadonlyArray<AnyFeature> = []>(config?: QueryClientConfig & {
readonly features?: TFeatures;
readonly links?: TLinks;
}): QueryClient<{
readonly meta: MetaOf<[...TLinks, ...TFeatures]>;
}>
Create a query client. Behaviors are opt-in: pass links and features,
or spread recommended(). The client's meta type is what they declare.
createQueryClient, function recommended(): RecommendedThe behaviors most apps want: retries, refetching on focus and reconnect,
and polling. Spread it into the config:
recommended } from "@raf-query/react";
export function function makeQueryClient(): QueryClient<{
readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
makeQueryClient() {
return createQueryClient<readonly [Link<{
readonly meta: RetryMeta;
}>], readonly [Feature<{
readonly meta: FocusRefetchMeta;
}>, Feature<{
readonly meta: ReconnectRefetchMeta;
}>, Feature<{
readonly meta: PollingMeta;
}>]>(config?: QueryClientConfig & {
readonly features?: readonly [Feature<{
readonly meta: FocusRefetchMeta;
}>, Feature<{
readonly meta: ReconnectRefetchMeta;
}>, Feature<...>] | undefined;
readonly links?: readonly [...] | undefined;
}): QueryClient<...>
Create a query client. Behaviors are opt-in: pass links and features,
or spread recommended(). The client's meta type is what they declare.
createQueryClient({ ...function recommended(): RecommendedThe behaviors most apps want: retries, refetching on focus and reconnect,
and polling. Spread it into the config:
recommended(), QueryClientConfig.staleTime?: number | undefinedDefault staleTime: 0.
staleTime: 30_000 });
}import { function QueryDevtools(props: QueryDevtoolsProps): React.JSX.ElementA toggle button that opens the devtools panel over the page.
QueryDevtools } from "@raf-query/react";
import { function setupRouterIntegration<TRouter extends AnyRouter>(options: RouterIntegrationOptions<TRouter>): voidConnects a query client to a TanStack Router (and TanStack Start) router:
setupRouterIntegration } from "@raf-query/react/tanstack-router";
import { const createRouter: CreateRouterFnCreates a new Router instance for React.
createRouter } from "@tanstack/react-router";
import { function Providers(props: {
readonly children: React.ReactNode;
readonly client?: QueryClient;
}): React.JSX.Element
The app's query client: the router's when it makes one, or one created
here, per server render and per browser tab.
Providers } from "./providers.tsx";
import { function makeQueryClient(): QueryClient<{
readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
makeQueryClient } from "./query-client.ts";
import { const routeTree: Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>
routeTree } from "./routeTree.gen.ts";
export function function getRouter(): RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>
getRouter() {
const const queryClient: QueryClient<{
readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
queryClient = function makeQueryClient(): QueryClient<{
readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
makeQueryClient();
const const router: RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>
router = createRouter<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>(options: RouterConstructorOptions<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, AnyContext, AnyContext, ... 6 more ..., undefined>, "never", false, RouterHistory, Record<...>>): RouterCore<...>
Creates a new Router instance for React.
createRouter({
context: {
queryClient: QueryClient;
}
context: { queryClient: QueryClient<any>queryClient },
defaultPreload?: false | "intent" | "viewport" | "render" | undefinedIf false, routes will not be preloaded by default in any way.
defaultPreload: "intent",
// The query cache decides what's fresh; loaders always run and dedupe there.
defaultPreloadStaleTime?: number | undefinedThe default preloadStaleTime a route should use if no preloadStaleTime is provided.
defaultPreloadStaleTime: 0,
routeTree?: Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined> | undefined
The route tree that will be used to configure the router instance.
routeTree,
scrollRestoration?: boolean | ((opts: {
location: ParsedLocation;
}) => boolean) | undefined
If true, scroll restoration will be enabled
scrollRestoration: true,
Wrap?: ((props: {
children: any;
}) => React.JSX.Element) | undefined
A component that will be used to wrap the entire router.
Wrap: (props: {
children: any;
}
props) => (
<function Providers(props: {
readonly children: React.ReactNode;
readonly client?: QueryClient;
}): React.JSX.Element
The app's query client: the router's when it makes one, or one created
here, per server render and per browser tab.
Providers client?: QueryClient<any> | undefinedclient={const queryClient: QueryClient<{
readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
queryClient}>
{props: {
children: any;
}
props.children: anychildren}
{import.meta.ImportMeta.env: ImportMetaEnvenv.ImportMetaEnv.DEV: booleanDEV ? <function QueryDevtools(props: QueryDevtoolsProps): React.JSX.ElementA toggle button that opens the devtools panel over the page.
QueryDevtools /> : null}
</function Providers(props: {
readonly children: React.ReactNode;
readonly client?: QueryClient;
}): React.JSX.Element
The app's query client: the router's when it makes one, or one created
here, per server render and per browser tab.
Providers>
),
});
setupRouterIntegration<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>>(options: RouterIntegrationOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, ... 8 more ..., undefined>, "never", false, RouterHistory, Record<...>>>): void
Connects a query client to a TanStack Router (and TanStack Start) router:
setupRouterIntegration({ RouterIntegrationOptions<TRouter extends AnyRouter>.queryClient: QueryClient<any>queryClient, RouterIntegrationOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, "never", false, RouterHistory, Record<...>>>.router: RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>
router });
return const router: RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>
router;
}makeQueryClient is a function, not a client at module level: a module-level client on the server would share one cache between every request. Providers is the provider component every framework shares; here it gets the router's client, the one loaders use:
"use client";
import { type interface QueryClient<TTypes extends RootTypes = any>A query cache. TTypes is its root's types, which factories built with
bindQueryOptions<typeof makeQueryClient>() are checked against. The bare
QueryClient type takes a client of any root.
QueryClient, function QueryClientProvider(props: {
readonly children?: ReactNode;
readonly client: QueryClient;
}): React.JSX.Element
Provides a client to the tree, and runs its features (focus and
reconnect refetching, polling, …) while mounted.
QueryClientProvider } from "@raf-query/react";
import { type type ReactNode = string | number | bigint | boolean | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | React.ReactPortal | Promise<AwaitedReactNode> | null | undefinedRepresents all of the things React can render.
ReactNode, function useState<S>(initialState: S | (() => S)): [S, React.Dispatch<React.SetStateAction<S>>] (+1 overload)Returns a stateful value, and a function to update it.
useState } from "react";
import { function makeQueryClient(): QueryClient<{
readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
makeQueryClient } from "~/query-client.ts";
/**
* The app's query client: the router's when it makes one, or one created
* here, per server render and per browser tab.
*/
export function function Providers(props: {
readonly children: ReactNode;
readonly client?: QueryClient;
}): React.JSX.Element
The app's query client: the router's when it makes one, or one created
here, per server render and per browser tab.
Providers(props: {
readonly children: ReactNode;
readonly client?: QueryClient;
}
props: {
readonly children: ReactNodechildren: type ReactNode = string | number | bigint | boolean | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | React.ReactPortal | Promise<AwaitedReactNode> | null | undefinedRepresents all of the things React can render.
ReactNode;
readonly client?: QueryClient<any> | undefinedclient?: interface QueryClient<TTypes extends RootTypes = any>A query cache. TTypes is its root's types, which factories built with
bindQueryOptions<typeof makeQueryClient>() are checked against. The bare
QueryClient type takes a client of any root.
QueryClient;
}) {
const [const client: QueryClient<any> | QueryClient<{
readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
client] = useState<QueryClient<any> | QueryClient<{
readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>>(initialState: QueryClient<any> | QueryClient<{
readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}> | (() => QueryClient<...> | QueryClient<...>)): [...] (+1 overload)
Returns a stateful value, and a function to update it.
useState(() => props: {
readonly children: ReactNode;
readonly client?: QueryClient;
}
props.client?: QueryClient<any> | undefinedclient ?? function makeQueryClient(): QueryClient<{
readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
makeQueryClient());
return (
<function QueryClientProvider(props: {
readonly children?: ReactNode;
readonly client: QueryClient;
}): React.JSX.Element
Provides a client to the tree, and runs its features (focus and
reconnect refetching, polling, …) while mounted.
QueryClientProvider client: QueryClient<any>client={const client: QueryClient<any> | QueryClient<{
readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
client}>{props: {
readonly children: ReactNode;
readonly client?: QueryClient;
}
props.children: ReactNodechildren}</function QueryClientProvider(props: {
readonly children?: ReactNode;
readonly client: QueryClient;
}): React.JSX.Element
Provides a client to the tree, and runs its features (focus and
reconnect refetching, polling, …) while mounted.
QueryClientProvider>
);
}Mount DevTools only when import.meta.env.DEV is true so Vite drops the panel from production builds. The example app always renders it, so the panel stays available in the deployed demo.
Type the router context in the root route, so loaders see queryClient:
import type { interface QueryClient<TTypes extends RootTypes = any>A query cache. TTypes is its root's types, which factories built with
bindQueryOptions<typeof makeQueryClient>() are checked against. The bare
QueryClient type takes a client of any root.
QueryClient } from "@raf-query/react";
import { function createRootRouteWithContext<TRouterContext extends {}>(): <TRegister = Register, TRouteContextFn = AnyContext, TBeforeLoadFn = AnyContext, TSearchValidator = undefined, TLoaderDeps extends Record<string, any> = {}, TLoaderFn = undefined, TSSR = unknown, TServerMiddlewares = unknown>(options?: RootRouteOptions<TRegister, TSearchValidator, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TSSR, TServerMiddlewares>) => RootRoute<TRegister, TSearchValidator, TRouterContext, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, unknown, unknown, TSSR, TServerMiddlewares, undefined>Creates a root route factory that requires a router context type.
createRootRouteWithContext } from "@tanstack/react-router";
export const const Route: RootRoute<Register, undefined, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>
Route = createRootRouteWithContext<{
queryClient: QueryClient;
}>(): <TRegister, TRouteContextFn, TBeforeLoadFn, TSearchValidator, TLoaderDeps, TLoaderFn, TSSR, TServerMiddlewares>(options?: RootRouteOptions<TRegister, TSearchValidator, {
queryClient: QueryClient;
}, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TSSR, TServerMiddlewares, undefined> | undefined) => RootRoute<...>
Creates a root route factory that requires a router context type.
createRootRouteWithContext<{ queryClient: QueryClient<any>queryClient: interface QueryClient<TTypes extends RootTypes = any>A query cache. TTypes is its root's types, which factories built with
bindQueryOptions<typeof makeQueryClient>() are checked against. The bare
QueryClient type takes a client of any root.
QueryClient }>()(
{},
);getRouter runs once per request on the server and once in the browser, so each request gets its own client and the browser keeps one for the session.
What the integration does
setupRouterIntegration:
- Streams queries during SSR. Entries go to the browser as they settle, through the router's own stream. Pending queries cross as promises.
- Clears the request's cache when the response finishes.
- Turns
redirect()thrown from aqueryFninto a navigation, wherever the query failed: a loader, a render or a background refetch in the browser.
Options:
| Option | Use |
|---|---|
queryClient | The router's client |
router | The router |
dehydrateOptions | filter which entries are sent; keepErrors to send a server request's error instead of a redacted one |
handleRedirects | false to handle redirects yourself |
A query whose server request throws reaches the browser as a RedactedError, which names the query but not the cause, so server errors don't leak into the page.
A component that reads a query no loader started, such as a waterfallHandle's, fetches it on the server while rendering. The integration streams that entry too, so the HTML has the data and the browser doesn't fetch it again. It's still a waterfall: the request starts only once the component renders. See SSR and hydration.
Loaders
A loader starts every query its route needs with load and returns the handles:
import {
function invalidateMany<TFactory extends QueryFactory>(client: ClientFor<TFactory>, factory: TFactory, partial?: PartialInput<TFactory>): Promise<void> (+3 overloads)Mark queries stale: a factory's (optionally only those whose input
matches partial), a group's (keys that start with its path, whether
the group built them or not), or those a target's where picks. Read
queries refetch now; the rest when next read. Resolves when the
refetches settle.
invalidateMany,
function load<const TEntries extends LoadEntries>(client: QueryClient, entries: TEntries): Promise<Loaded<TEntries>>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,
function prefetch<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadEntryArgs<TFactory>): LoadEntry<TFactory, "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,
type 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,
function swr<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadEntryArgs<TFactory>): LoadEntry<TFactory, "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 useInfiniteQuery<TFactory extends InfiniteQueryFactory>(factory: TFactory, handle: QueryHandle<TFactory>, options?: UseQueryOptions): readonly [QueryValue<TFactory>, InfiniteQueryState<TFactory>]Read a paged query (infiniteQueryOptions) by its handle. The data is
{ pages, pageParams }, and its state loads more pages.
useInfiniteQuery,
function useQuery<TFactory extends SingleQueryFactory>(factory: TFactory, handle: QueryHandle<TFactory>, options?: UseQueryOptions): readonly [QueryValue<TFactory>, QueryState<TFactory>]Read a query by its handle.
useQuery,
function useQueryClient(): QueryClientThe client from the nearest
QueryClientProvider.
useQueryClient,
} from "@raf-query/react";
import { function createFileRoute<TFilePath extends keyof FileRoutesByPath, TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]["parentRoute"], TId extends RouteConstraints["TId"] = FileRoutesByPath[TFilePath]["id"], TPath extends RouteConstraints["TPath"] = FileRoutesByPath[TFilePath]["path"], TFullPath extends RouteConstraints["TFullPath"] = FileRoutesByPath[TFilePath]["fullPath"]>(path?: TFilePath): FileRoute<TFilePath, TParentRoute, TId, TPath, TFullPath>["createRoute"]Creates a file-based Route factory for a given path.
createFileRoute } from "@tanstack/react-router";
import { const Suspense: React.ExoticComponent<React.SuspenseProps>Lets you display a fallback until its children have finished loading.
Suspense, function use<T>(usable: React.Usable<T>): Tuse } from "react";
import { function addComment(input: {
body: string;
slug: string;
}): Promise<PostComment>
addComment } from "~/api.ts";
import { function CommentsSkeleton(): React.JSX.ElementCommentsSkeleton, function renderComment(comment: PostComment): React.JSX.ElementrenderComment } from "~/components/comments.tsx";
import { function commentsQuery(input: {
slug: string;
}): InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery, function postQuery(input: {
slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery } from "~/queries.ts";
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,
});
function function PostPage(): React.JSX.ElementPostPage() {
const const data: {
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>>;
}
data = 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();
// The route already waited for this, so this is the post itself.
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 data: {
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>>;
}
data.post: QueryHandle<(input: {
slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
post);
const post = 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>
<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.JSX.ElementCommentsSkeleton />}>
<function Comments(props: {
handle: QueryHandle<typeof commentsQuery>;
}): React.JSX.Element
Comments handle: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
handle={const data: {
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>>;
}
data.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>
<function AddComment(props: {
slug: string;
}): React.JSX.Element
AddComment slug: stringslug={const post: Postpost.Post.slug: stringslug} />
</React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article>
);
}
function function Comments(props: {
handle: QueryHandle<typeof commentsQuery>;
}): React.JSX.Element
Comments(props: {
handle: QueryHandle<typeof commentsQuery>;
}
props: { handle: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, 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 commentsQuery(input: {
slug: string;
}): InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery> }) {
// May still be loading. If so, use() suspends until it lands.
const [const pagesThenable: QueryValue<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
pagesThenable, const commentsState: InfiniteQueryState<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
commentsState] = useInfiniteQuery<(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>, handle: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>, options?: UseQueryOptions): readonly [...]
Read a paged query (infiniteQueryOptions) by its handle. The data is
{ pages, pageParams }, and its state loads more pages.
useInfiniteQuery(
function commentsQuery(input: {
slug: string;
}): InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery,
props: {
handle: QueryHandle<typeof commentsQuery>;
}
props.handle: QueryHandle<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
handle,
);
const pages = use<InfiniteData<CommentPage, number>>(usable: React.Usable<InfiniteData<CommentPage, number>>): InfiniteData<CommentPage, number>use(const pagesThenable: QueryValue<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
pagesThenable); return (
<>
{const pages: InfiniteData<CommentPage, number>pages.InfiniteData<CommentPage, number>.pages: readonly CommentPage[]pages.ReadonlyArray<CommentPage>.flatMap<PostComment, undefined>(callback: (this: undefined, value: CommentPage, index: number, array: CommentPage[]) => PostComment | readonly PostComment[], thisArg?: undefined): PostComment[]Calls a defined callback function on each element of an array. Then, flattens the result into
a new array.
This is identical to a map followed by flat with depth 1.
flatMap((page: CommentPagepage) => page: CommentPagepage.CommentPage.items: readonly PostComment[]items).Array<PostComment>.map<React.JSX.Element>(callbackfn: (value: PostComment, index: number, array: PostComment[]) => React.JSX.Element, thisArg?: any): React.JSX.Element[]Calls a defined callback function on each element of an array, and returns an array that contains the results.
map(function renderComment(comment: PostComment): React.JSX.ElementrenderComment)}
{const commentsState: InfiniteQueryState<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
commentsState.PageControls.hasNextPage: booleanhasNextPage ? (
<React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button
React.ButtonHTMLAttributes<HTMLButtonElement>.disabled?: boolean | undefineddisabled={const commentsState: InfiniteQueryState<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
commentsState.PageControls.isFetchingNextPage: booleanisFetchingNextPage}
React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() => const commentsState: InfiniteQueryState<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
commentsState.PageControls.fetchNextPage(): Promise<void>Load the page after the last one. Resolves when it's there.
fetchNextPage()}
>
{const commentsState: InfiniteQueryState<(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
commentsState.PageControls.isFetchingNextPage: booleanisFetchingNextPage ? "Loading…" : "Load more"}
</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>
) : null}
</>
);
}
function function AddComment(props: {
slug: string;
}): React.JSX.Element
AddComment(props: {
slug: string;
}
props: { slug: stringslug: string }) {
const const queryClient: QueryClient<any>queryClient = function useQueryClient(): QueryClientThe client from the nearest
QueryClientProvider.
useQueryClient();
return (
<React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>form
React.FormHTMLAttributes<HTMLFormElement>.action?: string | ((formData: FormData) => void | Promise<void>) | undefinedaction={async (formData: FormDataformData) => {
await function addComment(input: {
body: string;
slug: string;
}): Promise<PostComment>
addComment({
body: stringbody: var String: StringConstructor
(value?: any) => string
Allows manipulation and formatting of text strings and determination and location of substrings within strings.
String(formData: FormDataformData.FormData.get(name: string): FormDataEntryValue | nullThe get() method of the FormData interface returns the first value associated with a given key from within a FormData object. If you expect multiple values and want all of them, use the getAll() method instead.
get("body")),
slug: stringslug: props: {
slug: string;
}
props.slug: stringslug,
});
await invalidateMany<any, readonly [readonly [(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, {
readonly slug: string;
}]]>(client: QueryClient<any>, targets: readonly [readonly [(input: {
slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, {
...;
}]]): Promise<void> (+3 overloads)
Mark queries stale: a factory's (optionally only those whose input
matches partial), a group's (keys that start with its path, whether
the group built them or not), or those a target's where picks. Read
queries refetch now; the rest when next read. Resolves when the
refetches settle.
invalidateMany(const queryClient: QueryClient<any>queryClient, [
[function commentsQuery(input: {
slug: string;
}): InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery, { slug?: string | undefinedslug: props: {
slug: string;
}
props.slug: stringslug }],
]);
}}
>
<React.JSX.IntrinsicElements.textarea: React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>textarea React.TextareaHTMLAttributes<HTMLTextAreaElement>.name?: string | undefinedname="body" React.TextareaHTMLAttributes<HTMLTextAreaElement>.required?: boolean | undefinedrequired />
<React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>Add comment</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>
</React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>form>
);
}See Starting queries for choosing between prefetch, swr and revalidate.
Handles in route context
beforeLoad can return handles too. They're part of the route context, so child routes' loaders and components read them, and the router serializes them to the browser with the rest of the context:
import { function load<const TEntries extends LoadEntries>(client: QueryClient, entries: TEntries): Promise<Loaded<TEntries>>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, function swr<TFactory extends QueryFactory>(factory: TFactory, ...args: LoadEntryArgs<TFactory>): LoadEntry<TFactory, "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 } from "@raf-query/react";
import { function createFileRoute<TFilePath extends keyof FileRoutesByPath, TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]["parentRoute"], TId extends RouteConstraints["TId"] = FileRoutesByPath[TFilePath]["id"], TPath extends RouteConstraints["TPath"] = FileRoutesByPath[TFilePath]["path"], TFullPath extends RouteConstraints["TFullPath"] = FileRoutesByPath[TFilePath]["fullPath"]>(path?: TFilePath): FileRoute<TFilePath, TParentRoute, TId, TPath, TFullPath>["createRoute"]Creates a file-based Route factory for a given path.
createFileRoute, function redirect<TRouter extends AnyRouter = RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>, const TTo extends string | undefined = ".", const TFrom extends string = string, const TMaskFrom extends string = TFrom, const TMaskTo extends string = "">(opts: RedirectOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>): Redirect<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>
Create a redirect Response understood by TanStack Router.
redirect } from "@tanstack/react-router";
import { function meQuery(): QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>meQuery } from "~/queries.ts";
export const const Route: Route<Register, RootRoute<Register, undefined, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "", "/", "/_auth", "/_auth", undefined, ResolveParams<"">, AnyContext, AnyContext, (args: BeforeLoadContextOptions<Register, RootRoute<Register, undefined, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, ... 4 more ..., undefined>, ... 6 more ..., undefined>) => Promise<...>, ... 6 more ..., undefined>
Route = createFileRoute<"/_auth", RootRoute<Register, undefined, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/_auth", "", "/">(path?: "/_auth" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<"", TParams> & FilebaseRouteOptionsInterface<...> & UpdatableRouteOptions<...>) | undefined) => Route<...>
Creates a file-based Route factory for a given path.
createFileRoute("/_auth")({
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 12 more ..., undefined>.beforeLoad?: Constrain<(args: BeforeLoadContextOptions<Register, RootRoute<Register, undefined, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, ... 4 more ..., undefined>, ... 6 more ..., undefined>) => Promise<...>, (ctx: BeforeLoadContextOptions<...>) => Promise<...>> | undefined
beforeLoad: async (args: BeforeLoadContextOptions<Register, RootRoute<Register, undefined, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, undefined, ResolveParams<"">, AnyContext, AnyContext, "/_auth", unknown, undefined>
args) => {
const [const handles: {
readonly me: QueryHandle<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>>;
}
handles, const data: LoadedData<{
readonly me: LoadEntry<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>, "swr">;
}>
data] = await load<{
readonly me: LoadEntry<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>, "swr">;
}>(client: QueryClient, entries: {
readonly me: LoadEntry<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>, "swr">;
}): Promise<Loaded<{
readonly me: LoadEntry<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>, "swr">;
}>>
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: BeforeLoadContextOptions<Register, RootRoute<Register, undefined, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, undefined, ResolveParams<"">, AnyContext, AnyContext, "/_auth", unknown, undefined>
args.BeforeLoadContextOptions<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 6 more ..., undefined>.context: {
queryClient: QueryClient;
}
context.queryClient: QueryClient<any>queryClient, {
me: LoadEntry<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>, "swr">me: swr<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>>(factory: () => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>, ...args: LoadEntryArgs<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>>): LoadEntry<() => QueryOptions<readonly ["account", "me"], User | null, User | null, 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 meQuery(): QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>meQuery),
});
if (const data: LoadedData<{
readonly me: LoadEntry<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>, "swr">;
}>
data.me: User | nullme === null) {
throw redirect<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>, "/login", string, string, "">(opts: RedirectOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, ... 8 more ..., undefined>, "never", false, RouterHistory, Record<...>>, string, "/login", string, "">): Redirect<...>
Create a redirect Response understood by TanStack Router.
redirect({ RequiredToOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, "never", false, RouterHistory, Record<...>>, string, "/login">.to: "/login" | "/posts" | "/settings" | "/blog/$slug" | "/posts/$slug" | "/" | "." | ".." | "/dashboard"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: "/login" });
}
return { me: QueryHandle<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>>me: const handles: {
readonly me: QueryHandle<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>>;
}
handles.me: QueryHandle<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>>me };
},
});function function Dashboard(): React.JSX.ElementDashboard() {
const const context: {
queryClient: QueryClient;
me: QueryHandle<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>>;
}
context = const Route: Route<Register, Route<Register, RootRoute<Register, undefined, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "", "/", "/_auth", "/_auth", undefined, ResolveParams<"">, AnyContext, AnyContext, (args: BeforeLoadContextOptions<Register, RootRoute<Register, undefined, {
queryClient: QueryClient;
}, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 6 more ..., undefined>) => Promise<...>, ... 6 more ..., undefined>, ... 15 more ..., undefined>
Route.RouteExtensions<"/_auth/dashboard", "/dashboard">.useRouteContext: <RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>, unknown>(opts?: UseRouteContextBaseOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
queryClient: QueryClient;
}, ... 8 more ..., undefined>, "never", false, RouterHistory, Record<...>>, "/_auth/dashboard", true, unknown> | undefined) => {
...;
}
useRouteContext();
const [const meThenable: QueryValue<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>>meThenable] = useQuery<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>>(factory: () => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>, handle: QueryHandle<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>>, options?: UseQueryOptions): readonly [QueryValue<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>>, QueryState<...>]Read a query by its handle.
useQuery(function meQuery(): QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>meQuery, const context: {
queryClient: QueryClient;
me: QueryHandle<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>>;
}
context.me: QueryHandle<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>>me);
const const me: User | nullme = use<User | null>(usable: React.Usable<User | null>): User | nulluse(const meThenable: QueryValue<() => QueryOptions<readonly ["account", "me"], User | null, User | null, DefaultTypes>>meThenable);
return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>{const me: User | nullme?.User.name: string | undefinedname}</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>;
}Importing @raf-query/react/tanstack-router registers handles as serializable, so Start's type check of beforeLoad's return accepts them.
Router settings
defaultPreload: "intent"runs a route's loader when the user hovers or focuses a link, so the queries start before the click.defaultPreloadStaleTime: 0makes the router run the loader again on click. Otherwise it reuses a hover's preload for 30 seconds, and the query cache never hears about the navigation. The query cache dedupes, so running the loader again is free when the data is fresh.
Pending UI
While a loader's swr entry is still loading, the previous page stays on screen. Show that a navigation is in progress with the router — not useIsFetching:
pendingComponentandpendingMsshow a full-page skeleton after the delay (1 second by default) for routes whose loader waits for slow data.useRouterState({ select: (state) => state.isLoading })drives a progress bar or spinner in the layout for any pending navigation:
function function NavigationProgress(): React.JSX.Element | nullNavigationProgress() {
const const isLoading: booleanisLoading = function useRouterState<boolean>(options: {
readonly select: (state: {
readonly isLoading: boolean;
}) => boolean;
}): boolean
useRouterState({ select: (state: {
readonly isLoading: boolean;
}) => boolean
select: (state: {
readonly isLoading: boolean;
}
state) => state: {
readonly isLoading: boolean;
}
state.isLoading: booleanisLoading });
return const isLoading: booleanisLoading ? <function ProgressBar(): ReactNodeProgressBar /> : null;
}Per-query background refetches after the page has rendered use isFetching on the query's state. See Background fetching indicators.
Search params
Use loaderDeps to pass validated search params to the loader, so it reruns when they change. See URL state and pagination.
Server and browser requests
A loader runs on the server during SSR and in the browser after, so a factory's queryFn runs in both.
Server functions
Factories can call server functions (createServerFn) directly, and get both sides for free. During SSR the call is a function call that sees the page request; in the browser, refetches call the same function over HTTP, with the browser's cookies.
export function function postQuery(input: {
slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery(input: {
slug: string;
}
input: { slug: stringslug: string }) {
return queryOptions<readonly ["posts", "bySlug", string], Post, Post>(options: BaseOptions<readonly ["posts", "bySlug", string], DefaultTypes> & {
readonly queryFn: (context: QueryFunctionContext<readonly ["posts", "bySlug", string]>) => Awaitable<Post>;
readonly select?: ((data: Post) => Post) | undefined;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes> (+1 overload)
Typed options for a query factory:
queryOptions({
BaseOptions<readonly ["posts", "bySlug", string], DefaultTypes>.queryKey: readonly ["posts", "bySlug", string]A plain array, usually built by a group(). Handles keep its literal type.
queryKey: const posts: Group
<readonly ["bySlug", string]>(rest_0: "bySlug", rest_1: string) => readonly ["posts", "bySlug", string]
posts("bySlug", input: {
slug: string;
}
input.slug: stringslug),
queryFn: (context: QueryFunctionContext<readonly ["posts", "bySlug", string]>) => Awaitable<Post>queryFn: () => function getPost(opts: RequiredFetcherDataOptions<undefined, ZodObject<{
slug: ZodString;
}, $strip>>): Promise<Post>
getPost({ RequiredFetcherDataOptions<undefined, ZodObject<{ slug: ZodString; }, $strip>>.data: {
slug: string;
}
data: input: {
slug: string;
}
input }),
});
}
Your own API
A factory that calls an API needs the user's cookies on both sides. The browser sends them; during SSR, forward the page request's:
import { function createIsomorphicFn(): IsomorphicFnBasecreateIsomorphicFn } from "@tanstack/react-start";
import { function getRequestHeader(name: RequestHeaderName): string | undefinedgetRequestHeader } from "@tanstack/react-start/server";
export const const forwardedHeaders: IsomorphicFn<[], Record<string, string>, Record<string, string>>forwardedHeaders = function createIsomorphicFn(): IsomorphicFnBasecreateIsomorphicFn()
.IsomorphicFnBase.server: <[], Record<string, string>>(serverImpl: () => Record<string, string>) => ServerOnlyFn<[], Record<string, string>>server((): type Record<K extends keyof any, T> = { [P in K]: T; }Construct a type with a set of properties K of type T
Record<string, string> => {
const const cookie: string | undefinedcookie = function getRequestHeader(name: RequestHeaderName): string | undefinedgetRequestHeader("cookie");
return const cookie: string | undefinedcookie === var undefinedundefined ? {} : { cookie: stringcookie };
})
.ServerOnlyFn<[], Record<string, string>>.client: <Record<string, string>>(clientImpl: () => Record<string, string>) => IsomorphicFn<[], Record<string, string>, Record<string, string>>client((): type Record<K extends keyof any, T> = { [P in K]: T; }Construct a type with a set of properties K of type T
Record<string, string> => ({}));- Send
forwardedHeaders()with each request: Hono'shcand tRPC's and oRPC's links take aheadersfunction. - Forward only what the API reads (
cookie,authorization), not every header. - For an API in the same app, call it in the process during SSR instead of over HTTP to your own origin.
examples/tanstack-startpicks an in-process client on the server and an HTTP one in the browser with abrowserexport condition, so the API's code stays out of the browser bundle.
Static prerendering
These docs are a TanStack Start app, prerendered to static HTML. Every page's loader waits for its content with swr during the build, the content crosses in the page's HTML, and later navigations load it in the browser. See the source.
Example
examples/tanstack-start is a complete issue tracker: lists with filters and infinite scroll, details with comments, and mutations with optimistic updates.