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:
- Client-only: SPA Mode (
ssr: false) withclientLoader, or Data Mode'screateBrowserRouter. Loaders run in the browser, on the tab's one query client, and nothing is hydrated.prefetch,swrandrevalidatework as in Starting queries. - Server rendering: Framework Mode with
ssr: true(the default) andloader. Loaders run on the server with one query client per request, which a root middleware creates, and dehydrate it into the loader data.HydrationBoundarywrites it into the browser's client. Useprefetchandrevalidatethere:swrwaits likerevalidate.
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:
export default {
ssr?: boolean | undefinedEnable 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:
// 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(): 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: 5000,
});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 | nullRenders the matching child route of a parent route or nothing if no child
route matches.
Outlet />
{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 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.
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;
}
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.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 ["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); 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>;
}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.
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((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:
swrholds 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.revalidatewaits whenever the data is stale. swrandrevalidatehandles read without suspending: their thenable is already fulfilled when the component renders.- React Router runs a route's
clientLoaderagain 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
loaderruns 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:
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: 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:
"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>
);
}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 | nullRenders the matching child route of a parent route or nothing if no child
route matches.
Outlet />
{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: 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:
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.
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.
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>();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>): voidSet a value for the context. If the context already has a value set, this
will overwrite it.
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:
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.
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): Responseredirect(`/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): DehydratedStateSnapshot 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:
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.ElementWrites server data into the browser (or SSR) client.
HydrationBoundary state: DehydratedStateA 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.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 ["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.ElementWrites 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); 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>;
}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.
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((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 aprefetchthat 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.
HydrationBoundarywrites 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.loadresolves to[handles, data].dataholds what therevalidateentries waited for, for deciding on a redirect or building another query's input in the loader. A component reads arevalidateentry's handle withuse()without suspending, and follows later refetches. See Handles.- Use
prefetchandrevalidatein aloader, notswr. Aloaderruns on the server for every navigation, client-side ones included, and its client starts empty, soswrwaits for the request every time, exactly likerevalidate. See Starting queries. loadrejects when arevalidateentry's request fails, so the route'sErrorBoundaryrenders 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 fromargs.request, the page'sRequest. - In the browser,
fetchsends 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(): UseNavigationResultReturns 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.
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.
NavLink to: ToCan be a string or a partial
Path:
to="/posts/hello">
{({ isPending: booleanIndicates if the pending
Location
matches the link's URL. Only
available in Framework/Data modes.
isPending }) => (isPending: booleanIndicates 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.
NavLink>;Per-query background refetches after the page has rendered use isFetching on the query's state. See Background fetching indicators.
Prefetching links
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, callload(queryClient, { … })withprefetchentries from the link'sonPointerEnter. 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.