Skip to content

raf-query is early. The API can still change between minor releases, so use it at your own risk.

raf-query

Integrations

React Router

Client-only apps start queries in clientLoader on the tab's client; server-rendered apps start them in loader, dehydrate the cache, and hydrate it with HydrationBoundary.

React Router apps load data in one of two places, and raf-query fits each differently. Find yours, then read its section. Each one stands on its own:

If you can choose the router, prefer TanStack Start. A link preloaded on hover runs its route's loaders in the browser, so the next page's queries start in the browser's cache and the click uses them. React Router's prefetch="intent" doesn't: see Prefetching links.

Client-only apps

SPA Mode

Turn off server rendering with ssr: false:

react-router.config.ts
export default {
	ssr?: boolean | undefined

Enable server-side rendering for your application. Disable to use "SPA Mode", which will request the / path at build-time and save it as an index.html file with your assets so your application can be deployed as a SPA without server-rendering. Default's to true.

ssr
: false,
} satisfies
type Config = {
    appDirectory?: string;
    serverModuleFormat?: ServerModuleFormat;
    future?: [keyof FutureConfig] extends [never] ? {
        [key: string]: never;
    } : Partial<FutureConfig>;
    splitRouteModules?: SplitRouteModulesOption;
    basename?: string;
    buildDirectory?: string;
    buildEnd?: BuildEndHook;
    prerender?: PrerenderPaths | {
        paths: PrerenderPaths;
        concurrency?: number;
    };
    ... 6 more ...;
    allowedActionOrigins?: string[];
}

Config to be exported via the default export from react-router.config.ts.

Config
;

Create the tab's query client in a module, so loaders can import it, and provide it in the root route. Its HydrateFallback is what the page shows until the first clientLoader is done:

app/query-client.ts
// Only in a client-only app: with server rendering, a module's client would
// be shared by every request.
export const 
const queryClient: QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
queryClient
=
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(): Recommended

The behaviors most apps want: retries, refetching on focus and reconnect, and polling. Spread it into the config:

recommended
(),
QueryClientConfig.staleTime?: number | undefined

Default staleTime: 0.

staleTime
: 5000,
});
app/root.tsx
export default function function App(): React.JSX.ElementApp() {
	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 queryClient: QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
queryClient
}>
<function Outlet(props: OutletProps): React.ReactElement | null

Renders the matching child route of a parent route or nothing if no child route matches.

@paramprops — Props@paramprops.context — n/a@returnsReact element for the rendered outlet or null if no child route matches.
Outlet
/>
{import.meta.ImportMeta.env: ImportMetaEnvenv.ImportMetaEnv.DEV: booleanDEV ? <function QueryDevtools(props: QueryDevtoolsProps): React.JSX.Element

A toggle button that opens the devtools panel over the page.

QueryDevtools
/> : null}
</
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
>
); } // Rendered into index.html at build time, until the first clientLoader is done. export function function HydrateFallback(): React.JSX.ElementHydrateFallback() { return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>; }

Nothing is on by default: recommended() adds retries, refetching on window focus and reconnect, and polling.

Mount DevTools only when import.meta.env.DEV is true so Vite drops the panel from production builds.

Client loaders

Each clientLoader starts its queries on that client with load and returns the handles. There is no snapshot to dehydrate and no HydrationBoundary: the queries are already in the client the components read.

app/routes/posts.$slug.tsx
export async function 
function clientLoader(args: Route.ClientLoaderArgs): Promise<{
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>;
}>
clientLoader
(
args: CreateClientLoaderArgs<Info & {
    module: typeof import("./posts.$slug");
    matches: Matches;
}>
args
: Route.
type Route.ClientLoaderArgs = ClientDataFunctionArgs<{
    slug: string;
}> & {
    serverLoader: () => Promise<undefined>;
}
ClientLoaderArgs
) {
const { const slug: stringslug } =
args: CreateClientLoaderArgs<Info & {
    module: typeof import("./posts.$slug");
    matches: Matches;
}>
args
.
params: {
    slug: string;
}

Dynamic route params for the current route.

params
;
const [
const handles: {
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>;
}
handles
] = await
load<{
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
    readonly post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "swr">;
}>(client: QueryClient, entries: {
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
    readonly post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, 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 queryClient: QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
queryClient
, {
// Doesn't wait: the comments stream in after the page renders.
comments: LoadEntry<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">
comments
:
prefetch<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>(factory: (input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, ...args: LoadEntryArgs<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["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 ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery
, { slug: stringslug }),
// Waits on the first visit only. Coming back shows the cached post at // once, and refetches it behind the page once it's stale.
post: LoadEntry<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "swr">
post
:
swr<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>(factory: (input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, ...args: LoadEntryArgs<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>): LoadEntry<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", 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 ["post", string], Post, Post, DefaultTypes>
postQuery
, { slug: stringslug }),
}); return
const handles: {
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>;
}
handles
;
}
app/routes/posts.$slug.tsx
export default function function PostPage(props: Route.ComponentProps): React.JSX.ElementPostPage(
props: CreateComponentProps<Info & {
    module: typeof import("./posts.$slug");
    matches: Matches;
}, false>
props
: Route.
type Route.ComponentProps = {
    params: {
        slug: string;
    };
    matches: [{
        id: "root";
        params: Record<string, string | undefined>;
        pathname: string;
        loaderData: undefined;
        handle: unknown;
    }, {
        id: "routes/posts.$slug";
        params: Record<string, string | undefined>;
        pathname: string;
        loaderData: {
            readonly comments: QueryHandle<(input: {
                slug: string;
            }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
            readonly post: QueryHandle<(input: {
                slug: string;
            }) => QueryOptions<...>>;
        };
        handle: unknown;
    }, ...({
        ...;
    } | undefined)[]];
} & {
    ...;
}
ComponentProps
) {
const {
const comments: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
comments
,
const post: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
post
} =
props: CreateComponentProps<Info & {
    module: typeof import("./posts.$slug");
    matches: Matches;
}, false>
props
.
loaderData: {
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<...>>;
}

The data returned from the loader or clientLoader

loaderData
;
return ( <React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> <
function Post(props: {
    handle: QueryHandle<typeof postQuery>;
}): React.JSX.Element
Post
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
handle
={
const post: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
post
} />
<const Suspense: React.ExoticComponent<React.SuspenseProps>

Lets you display a fallback until its children have finished loading.

Suspense
React.SuspenseProps.fallback?: React.ReactNode

A 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 ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
handle
={
const comments: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
comments
} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>

Lets you display a fallback until its children have finished loading.

Suspense
>
</React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> ); } function
function Post(props: {
    handle: QueryHandle<typeof postQuery>;
}): React.JSX.Element
Post
(
props: {
    handle: QueryHandle<typeof postQuery>;
}
props
: {
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, 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 postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
postQuery
> }) {
// The loader waited for the post, so this is the post, not a promise. const [
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
postThenable
] =
useQuery<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>(factory: (input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>, options?: UseQueryOptions): readonly [QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>, QueryState<...>]

Read a query by its handle.

useQuery
(
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
postQuery
,
props: {
    handle: QueryHandle<typeof postQuery>;
}
props
.
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
handle
);
const post = use<Post>(usable: React.Usable<Post>): Postuse(
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
postThenable
);
const post: Post
return <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>; }
app/components/comments.tsx
export 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 ["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 ["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 ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
pagesThenable
] =
useInfiniteQuery<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>(factory: (input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, handle: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["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 ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery
,
props: {
    handle: QueryHandle<typeof commentsQuery>;
}
props
.
handle: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
handle
);
const const pages: InfiniteData<CommentPage, number>pages = use<InfiniteData<CommentPage, number>>(usable: React.Usable<InfiniteData<CommentPage, number>>): InfiniteData<CommentPage, number>use(
const pagesThenable: QueryValue<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["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.

@paramcallback — A function that accepts up to three arguments. The flatMap method calls the callback function one time for each element in the array.@paramthisArg — An object to which the this keyword can refer in the callback function. If thisArg is omitted, undefined is used as the this value.
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.

@paramcallbackfn — A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg — An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
map
((comment: PostCommentcomment) => <
function Comment(props: {
    comment: PostComment;
}): React.ReactNode
Comment
React.Attributes.key?: React.Key | null | undefinedkey={comment: PostCommentcomment.PostComment.id: stringid} comment: PostCommentcomment={comment: PostCommentcomment} />);
}
  • The client outlives navigations, so the entries differ as in Starting queries: swr holds up only the first visit, and returning to a page shows the cached data at once, then refetches it behind the page once it's stale. revalidate waits whenever the data is stale.
  • swr and revalidate handles read without suspending: their thenable is already fulfilled when the component renders.
  • React Router runs a route's clientLoader again on every navigation to it. Data that is still fresh costs no request.
  • A module's client is safe only when nothing renders on the server. With ssr: true, every request would share it: see Server rendering.
  • In Data Mode, a route's loader runs in the browser too: start queries in it the same way, on a client from a module.

Server rendering

Provider

One function makes the app's clients:

app/query-client.ts
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(): Recommended

The 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(): Recommended

The behaviors most apps want: retries, refetching on focus and reconnect, and polling. Spread it into the config:

recommended
(), QueryClientConfig.staleTime?: number | undefined

Default staleTime: 0.

staleTime
: 5000 });
}

The root route's component renders Providers, the provider component every framework shares. Without a client prop, it creates one client per server render and one per browser tab:

app/providers.tsx
"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 | undefined

Represents 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 | undefined

Represents 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
>
); }
app/root.tsx
export default function function App(): React.JSX.ElementApp() {
	return (
		<
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
>
<function Outlet(props: OutletProps): React.ReactElement | null

Renders the matching child route of a parent route or nothing if no child route matches.

@paramprops — Props@paramprops.context — n/a@returnsReact element for the rendered outlet or null if no child route matches.
Outlet
/>
{import.meta.ImportMeta.env: ImportMetaEnvenv.ImportMetaEnv.DEV: booleanDEV ? <function QueryDevtools(props: QueryDevtoolsProps): React.JSX.Element

A toggle button that opens the devtools panel over the page.

QueryDevtools
/> : null}
</
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
>
); }

Nothing is on by default: recommended() adds retries, refetching on window focus and reconnect, and polling.

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.

Queries no loader started

Loaders start queries on the request's client below, and HydrationBoundary sends their data. A component that reads a query no loader started, such as a waterfallHandle's, fetches it on the server render's client while rendering, so the HTML has the data. Nothing sends that client's cache to the browser: a loader's snapshot holds only what loaders started. So the browser fetches the query again when it hydrates, and the query runs twice for the page. That's the cost of starting a query outside a loader. Start it in the loader to run it once (see SSR and hydration).

A client per request

A root middleware creates the request's query client and puts it in a context:

app/query-client.ts
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 createContext<T>(defaultValue?: T): RouterContext<T>

Creates a type-safe RouterContext object that can be used to store and retrieve arbitrary values in actions, loaders, and middleware. Similar to React's createContext, but specifically designed for React Router's request/response lifecycle.

@paramdefaultValue — An optional default value for the context. This value will be returned if no value has been set for this context.@returnsA RouterContext object that can be used with context.get() and context.set() in actions, loaders, and middleware.
createContext
} from "react-router";
export const const queryClientContext: RouterContext<QueryClient<any>>queryClientContext = createContext<QueryClient<any>>(defaultValue?: QueryClient<any> | undefined): RouterContext<QueryClient<any>>

Creates a type-safe RouterContext object that can be used to store and retrieve arbitrary values in actions, loaders, and middleware. Similar to React's createContext, but specifically designed for React Router's request/response lifecycle.

@paramdefaultValue — An optional default value for the context. This value will be returned if no value has been set for this context.@returnsA RouterContext object that can be used with context.get() and context.set() in actions, loaders, and middleware.
createContext
<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
>();
app/root.tsx
export const 
const middleware: CreateServerMiddlewareFunction<Info & {
    module: typeof import("./root");
    matches: Matches;
}>[]
middleware
: interface Array<T>Array<Route.
type Route.MiddlewareFunction = (args: ServerDataFunctionArgs<{
    slug?: undefined;
} | {
    slug: string;
} | {
    slug: string;
}>, next: MiddlewareNextFunction<Response>) => MaybePromise<void | Response>
MiddlewareFunction
> = [
({ context: Readonly<RouterContextProvider>

An instance of RouterContextProvider that can be used for type-safe access to context values set in your route middlewares. If you are using a custom server adapter, you may provide an initial set of context values from your getLoadContext function.

context
}) => {
context: Readonly<RouterContextProvider>

An instance of RouterContextProvider that can be used for type-safe access to context values set in your route middlewares. If you are using a custom server adapter, you may provide an initial set of context values from your getLoadContext function.

context
.set<RouterContext<QueryClient<any>>>(context: RouterContext<QueryClient<any>>, value: QueryClient<any>): void

Set a value for the context. If the context already has a value set, this will overwrite it.

@paramcontext — The context to set the value for@paramvalue — The value to set for the context@returns
set
(const queryClientContext: RouterContext<QueryClient<any>>queryClientContext,
function makeQueryClient(): QueryClient<{
    readonly meta: Flat<Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta> & Present<RetryMeta>>;
}>
makeQueryClient
());
}, ];

Server middleware runs for the document request and for each client navigation's data request, before any loader. Every loader of that request reads the same client from args.context, so nested routes share one cache: React Router runs the matched routes' loaders in parallel, and a query that a layout's loader and a child route's loader both start is requested once.

Server loaders

Each loader takes the request's client from the context, starts every query the route needs with load, and returns the handles with a dehydrated snapshot of the cache, from dehydrate:

app/routes/posts.$slug.tsx
export async function 
function loader(args: Route.LoaderArgs): Promise<{
    dehydratedState: DehydratedState;
    comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>;
}>
loader
(
args: CreateServerLoaderArgs<Info & {
    module: typeof import("./posts.$slug");
    matches: Matches;
}>
args
: Route.
type Route.LoaderArgs = {
    request: Request;
    url: URL;
    params: {
        slug: string;
    };
    pattern: string;
    context: Readonly<RouterContextProvider>;
}
LoaderArgs
) {
const const qc: QueryClient<any>qc =
args: CreateServerLoaderArgs<Info & {
    module: typeof import("./posts.$slug");
    matches: Matches;
}>
args
.context: Readonly<RouterContextProvider>

An instance of RouterContextProvider that can be used for type-safe access to context values set in your route middlewares. If you are using a custom server adapter, you may provide an initial set of context values from your getLoadContext function.

context
.get<QueryClient<any>>(context: RouterContext<QueryClient<any>>): QueryClient<any>

Access a value from the context. If no value has been set for the context, it will return the context's defaultValue if provided, or throw an error if no defaultValue was set.

@paramcontext — The context to get the value for@returnsThe value for the context, or the context's defaultValue if no value was set
get
(const queryClientContext: RouterContext<QueryClient<any>>queryClientContext);
const { const slug: stringslug } =
args: CreateServerLoaderArgs<Info & {
    module: typeof import("./posts.$slug");
    matches: Matches;
}>
args
.
params: {
    slug: string;
}

Dynamic route params for the current route.

params
;
const [
const handles: {
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>;
}
handles
,
const data: LoadedData<{
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
    readonly post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "revalidate">;
}>
data
] = await
load<{
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
    readonly post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "revalidate">;
}>(client: QueryClient, entries: {
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
    readonly post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "revalidate">;
}): 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, {
// Doesn't wait: the comments stream in after the page renders.
comments: LoadEntry<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">
comments
:
prefetch<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>(factory: (input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, ...args: LoadEntryArgs<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["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 ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery
, { slug: stringslug }),
// Waits, so the post is in the page's first HTML.
post: LoadEntry<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "revalidate">
post
:
revalidate<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>(factory: (input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, ...args: LoadEntryArgs<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>): LoadEntry<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "revalidate">

Wait for fresh data, as HTTP's must-revalidate does. Fresh cached data resolves at once without a refetch (staleTime is the max-age); with stale data or none it waits for the fetch. On a server client, data counts as fresh until a write invalidates it. The load rejects if the fetch fails.

revalidate
(
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
postQuery
, { slug: stringslug }),
}); // `data` holds what the loader waited for: here, the post. if (
const data: LoadedData<{
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
    readonly post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "revalidate">;
}>
data
.post: Postpost.Post.slug: stringslug !== const slug: stringslug) {
throw function redirect(url: string, init?: number | ResponseInit): Response

A redirect Response. Sets the status code and the Location header. Defaults to 302 Found.

@paramurl — The URL to redirect to.@paraminit — The status code or a ResponseInit object to be included in the response.@returnsA Response object with the redirect status and Location header.
redirect
(`/posts/${
const data: LoadedData<{
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
    readonly post: LoadEntry<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, "revalidate">;
}>
data
.post: Postpost.Post.slug: stringslug}`);
} return { ...
const handles: {
    readonly comments: QueryHandle<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>;
    readonly post: QueryHandle<(input: {
        slug: string;
    }) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>;
}
handles
, dehydratedState: DehydratedStatedehydratedState: function dehydrate(client: QueryClient, options?: DehydrateOptions): DehydratedState

Snapshot the client's entries for transfer.

dehydrate
(const qc: QueryClient<any>qc) };
}

The route component wraps what it renders in a HydrationBoundary with that snapshot, and passes the handles down:

app/routes/posts.$slug.tsx
export default function function PostPage(props: Route.ComponentProps): React.JSX.ElementPostPage(
props: CreateComponentProps<Info & {
    module: typeof import("./posts.$slug");
    matches: Matches;
}, false>
props
: Route.
type Route.ComponentProps = {
    params: {
        slug: string;
    };
    matches: [{
        id: "root";
        params: Record<string, string | undefined>;
        pathname: string;
        loaderData: undefined;
        handle: unknown;
    }, {
        id: "routes/posts.$slug";
        params: Record<string, string | undefined>;
        pathname: string;
        loaderData: {
            dehydratedState: {
                readonly entries: readonly {
                    readonly data?: undefined;
                    readonly promise?: Promise<{
                        readonly data?: undefined;
                        readonly updatedAt: number;
                    }> | undefined;
                    readonly queryKey: readonly undefined[];
                    readonly updatedAt: number;
                }[];
            };
            comments: {
                readonly input: {
                    slug: string;
                };
                readonly queryKey: ["comments", string];
            };
            post: {
                ...;
            };
        };
        handle: unknown;
    }, ...({
        ...;
    } | undefined)[]];
} & {
    ...;
}
ComponentProps
) {
const {
const comments: {
    readonly input: {
        slug: string;
    };
    readonly queryKey: ["comments", string];
}
comments
,
const dehydratedState: {
    readonly entries: readonly {
        readonly data?: undefined;
        readonly promise?: Promise<{
            readonly data?: undefined;
            readonly updatedAt: number;
        }> | undefined;
        readonly queryKey: readonly undefined[];
        readonly updatedAt: number;
    }[];
}
dehydratedState
,
const post: {
    readonly input: {
        slug: string;
    };
    readonly queryKey: ["post", string];
}
post
} =
props: CreateComponentProps<Info & {
    module: typeof import("./posts.$slug");
    matches: Matches;
}, false>
props
.
loaderData: {
    dehydratedState: {
        readonly entries: readonly {
            readonly data?: undefined;
            readonly promise?: Promise<{
                readonly data?: undefined;
                readonly updatedAt: number;
            }> | undefined;
            readonly queryKey: readonly undefined[];
            readonly updatedAt: number;
        }[];
    };
    comments: {
        readonly input: {
            slug: string;
        };
        readonly queryKey: ["comments", string];
    };
    post: {
        ...;
    };
}

The data returned from the loader or clientLoader

loaderData
;
return ( <function HydrationBoundary(props: HydrationBoundaryProps): React.JSX.Element

Writes server data into the browser (or SSR) client.

HydrationBoundary
state: DehydratedState

A loader's dehydrate(client).

state
={
const dehydratedState: {
    readonly entries: readonly {
        readonly data?: undefined;
        readonly promise?: Promise<{
            readonly data?: undefined;
            readonly updatedAt: number;
        }> | undefined;
        readonly queryKey: readonly undefined[];
        readonly updatedAt: number;
    }[];
}
dehydratedState
}>
<React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> <
function Post(props: {
    handle: QueryHandle<typeof postQuery>;
}): React.JSX.Element
Post
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
handle
={
const post: {
    readonly input: {
        slug: string;
    };
    readonly queryKey: ["post", string];
}
post
} />
<const Suspense: React.ExoticComponent<React.SuspenseProps>

Lets you display a fallback until its children have finished loading.

Suspense
React.SuspenseProps.fallback?: React.ReactNode

A 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 ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
handle
={
const comments: {
    readonly input: {
        slug: string;
    };
    readonly queryKey: ["comments", string];
}
comments
} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>

Lets you display a fallback until its children have finished loading.

Suspense
>
</React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> </function HydrationBoundary(props: HydrationBoundaryProps): React.JSX.Element

Writes server data into the browser (or SSR) client.

HydrationBoundary
>
); } function
function Post(props: {
    handle: QueryHandle<typeof postQuery>;
}): React.JSX.Element
Post
(
props: {
    handle: QueryHandle<typeof postQuery>;
}
props
: {
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, 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 postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
postQuery
> }) {
// The loader waited for the post, so this is the post, not a promise. const [
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
postThenable
] =
useQuery<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>(factory: (input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>, handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>, options?: UseQueryOptions): readonly [QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>, QueryState<...>]

Read a query by its handle.

useQuery
(
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>
postQuery
,
props: {
    handle: QueryHandle<typeof postQuery>;
}
props
.
handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
handle
);
const post = use<Post>(usable: React.Usable<Post>): Postuse(
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["post", string], Post, Post, DefaultTypes>>
postThenable
);
const post: Post
return <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>; }
app/components/comments.tsx
export 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 ["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 ["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 ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
pagesThenable
] =
useInfiniteQuery<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>(factory: (input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, handle: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["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 ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery
,
props: {
    handle: QueryHandle<typeof commentsQuery>;
}
props
.
handle: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
handle
);
const const pages: InfiniteData<CommentPage, number>pages = use<InfiniteData<CommentPage, number>>(usable: React.Usable<InfiniteData<CommentPage, number>>): InfiniteData<CommentPage, number>use(
const pagesThenable: QueryValue<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["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.

@paramcallback — A function that accepts up to three arguments. The flatMap method calls the callback function one time for each element in the array.@paramthisArg — An object to which the this keyword can refer in the callback function. If thisArg is omitted, undefined is used as the this value.
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.

@paramcallbackfn — A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg — An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
map
((comment: PostCommentcomment) => <
function Comment(props: {
    comment: PostComment;
}): React.ReactNode
Comment
React.Attributes.key?: React.Key | null | undefinedkey={comment: PostCommentcomment.PostComment.id: stringid} comment: PostCommentcomment={comment: PostCommentcomment} />);
}
  • Create the server's client per request, in the middleware. A module-level client would share data between requests and users.
  • dehydrate(qc) takes entries with data and entries still loading. React Router streams promises in loader data, so a prefetch that is still loading resolves in the browser when the server's request does, and the browser doesn't fetch it again.
  • With nested routes, a loader's snapshot holds every entry the client has when the loader returns, other loaders' queries included. The browser keeps the newer copy of an entry, so the overlap only adds to the payload.
  • HydrationBoundary writes the entries into the provider's client: entries the browser doesn't have yet while it renders, so its children read them, and the rest once it commits. The browser keeps whichever copy of an entry is newer. See SSR and hydration.
  • load resolves to [handles, data]. data holds what the revalidate entries waited for, for deciding on a redirect or building another query's input in the loader. A component reads a revalidate entry's handle with use() without suspending, and follows later refetches. See Handles.
  • Use prefetch and revalidate in a loader, not swr. A loader runs on the server for every navigation, client-side ones included, and its client starts empty, so swr waits for the request every time, exactly like revalidate. See Starting queries.
  • load rejects when a revalidate entry's request fails, so the route's ErrorBoundary renders it.

What the integration is

HydrationBoundary, from @raf-query/react, takes the dehydrated state. The cache verbs, handles and useQuery are the same as everywhere else. React Router has no hooks for streaming a cache with the page, so each loader returns its snapshot in its data. Options the TanStack Start integration takes as dehydrateOptions, such as keepErrors and filter, go to dehydrate(qc, options).

react-router isn't a peer dependency: raf-query imports nothing from it.

Server and browser requests

Loaders run on the server, for the first page load and for each client navigation, so their client starts with an empty cache every time. Components refetch from the browser. A factory's queryFn runs in both places, and has to reach the same data as the same user in both:

  • On the server, a relative fetch("/api/...") has no page origin to resolve against, and the browser's cookies aren't sent unless you forward them from args.request, the page's Request.
  • In the browser, fetch sends the user's cookies to your origin.

examples/react-router serves its API from a resource route and gives its API client the page request's cookies while a loader runs, in app/settings.ts and app/api.ts. Loaders call the API in the same process, and the browser calls it over HTTP.

Pending UI

While a link navigation is loading the next route, show pending state with React Router, not useIsFetching. useNavigation reports a navigation in progress, and NavLink reports its own:

function function NavigationSpinner(): React.JSX.Element | nullNavigationSpinner() {
	const const navigation: UseNavigationResultnavigation = function useNavigation(): UseNavigationResult

Returns the current Navigation, defaulting to an "idle" navigation when no navigation is in progress. You can use this to render pending UI (like a global spinner) or read FormData from a form navigation.

@returnsThe current Navigation object
useNavigation
();
return const navigation: UseNavigationResultnavigation.state: "idle" | "loading" | "submitting"state === "loading" ? <function Spinner(): ReactNodeSpinner /> : null; } <const NavLink: React.ForwardRefExoticComponent<NavLinkProps & React.RefAttributes<HTMLAnchorElement>>

Wraps <Link> with additional props for styling active and pending states.

@paramprops.caseSensitive — n/a@paramprops.children — n/a@paramprops.className — n/a@paramprops.discover — [modes: framework] n/a@paramprops.end — n/a@paramprops.prefetch — [modes: framework] n/a@paramprops.preventScrollReset — [modes: framework, data] n/a@paramprops.relative — n/a@paramprops.reloadDocument — n/a@paramprops.replace — n/a@paramprops.state — n/a@paramprops.style — n/a@paramprops.to — n/a@paramprops.viewTransition — [modes: framework, data] n/a
NavLink
to: To

Can be a string or a partial Path:

to
="/posts/hello">
{({ isPending: boolean

Indicates if the pending Location matches the link's URL. Only available in Framework/Data modes.

isPending
}) => (isPending: boolean

Indicates if the pending Location matches the link's URL. Only available in Framework/Data modes.

isPending
? "Hello (loading…)" : "Hello")}
</const NavLink: React.ForwardRefExoticComponent<NavLinkProps & React.RefAttributes<HTMLAnchorElement>>

Wraps <Link> with additional props for styling active and pending states.

@paramprops.caseSensitive — n/a@paramprops.children — n/a@paramprops.className — n/a@paramprops.discover — [modes: framework] n/a@paramprops.end — n/a@paramprops.prefetch — [modes: framework] n/a@paramprops.preventScrollReset — [modes: framework, data] n/a@paramprops.relative — n/a@paramprops.reloadDocument — n/a@paramprops.replace — n/a@paramprops.state — n/a@paramprops.style — n/a@paramprops.to — n/a@paramprops.viewTransition — [modes: framework, data] n/a
NavLink
>;

Per-query background refetches after the page has rendered use isFetching on the query's state. See Background fetching indicators.

prefetch="intent" on a Link or NavLink preloads the route's modules when the user hovers or focuses the link ("render" does it when the link renders, "viewport" when it scrolls into view, and "none", the default, never). With either kind of loader, the click doesn't start from the prefetch's queries:

  • A server loader: the prefetch also requests the loader data, which runs the loaders on the server, but React Router doesn't keep the response. The click requests the loader data again, and the loaders and their queries run a second time. What the prefetch saves is loading the route's code.
  • A clientLoader: the prefetch doesn't run it; it runs on the click. To start its queries earlier, call load(queryClient, { … }) with prefetch entries from the link's onPointerEnter. See Starting queries.

In TanStack Router, the navigation reuses a route preloaded in the last 30 seconds, or joins its loader if it's still running (Preloading). Its loaders run in the browser, so the queries a preload starts land in the browser's cache, and the navigation's loader finds them there even once the preload is stale. See TanStack Start.

Search params

Read search params from args.request.url in the loader or clientLoader, validate them, and pass them as the input. React Router runs the loader again when they change. See URL state and pagination for what to show while it loads.

Mutations

Write from the browser in a React action, then await invalidateOne(queryClient, factory, input) or invalidateMany with the client from useQueryClient(), as in Mutations. React Router actions work too: after one, React Router runs the page's loaders again. A server loader's new snapshot carries the new data. A clientLoader reads the tab's cache, which may still hold the old data as fresh, so await invalidateOne(queryClient, ...) in the clientAction before it returns.

Example

examples/react-router is the same issue tracker as the TanStack Start and Next.js examples, so the integrations can be compared side by side.