Skip to content

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

raf-query

Guides

Retries and errors

Retry failed requests with the retry() link, set attempts per query in meta, choose delays and which errors to retry, time requests out, and show errors.

The retry() link retries failed requests. With its defaults, a request that fails in the browser is retried 3 times, waiting 1, 2 and then 4 seconds, before the query counts as failed. Readers keep suspending, or keep showing their current data, the whole time.

Nothing retries unless the client has the link, on its own or through recommended():

const 
const queryClient: QueryClient<{
    readonly meta: Flat<Present<RetryMeta>>;
}>
queryClient
=
createQueryClient<readonly [Link<{
    readonly meta: RetryMeta;
}>], []>(config?: QueryClientConfig & {
    readonly features?: [] | undefined;
    readonly links?: readonly [Link<{
        readonly meta: RetryMeta;
    }>] | undefined;
}): QueryClient<{
    readonly meta: Flat<Present<RetryMeta>>;
}>

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
({
links?: (readonly AnyLink[] & readonly [Link<{
    readonly meta: RetryMeta;
}>]) | undefined

Wrap every request, outermost first.

links
: [
function retry(options?: RetryOptions): Link<{
    readonly meta: RetryMeta;
}>

Retry failed requests. Thrown Responses (router redirects and not-founds) and cancelled requests aren't retried, server clients never retry, and a browser that is offline waits until it's back online before retrying. A query sets its own attempts with meta: { retry }.

retry
()] });
// or const
const withEverything: QueryClient<{
    readonly meta: Flat<Present<RetryMeta> & Present<FocusRefetchMeta> & Present<ReconnectRefetchMeta> & Present<PollingMeta>>;
}>
withEverything
=
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
() });

Some requests are never retried:

  • Cancelled requests, whose signal was aborted.
  • Requests on a server client. The user is waiting for the HTML, and the browser can try again.

Attempts per query

A query sets its own attempts with meta: { retry }: a number of retries after the first attempt, or false for none. It wins over the link's attempts:

export function 
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery
(
input: {
    slug: string;
}
input
: { slug: stringslug: string }) {
return
queryOptions<readonly ["posts", "bySlug", string], Post, Post>(options: BaseOptions<readonly ["posts", "bySlug", string], DefaultTypes> & {
    readonly queryFn: (context: QueryFunctionContext<readonly ["posts", "bySlug", string]>) => Awaitable<Post>;
    readonly select?: ((data: Post) => Post) | undefined;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes> (+1 overload)

Typed options for a query factory:

queryOptions
({
BaseOptions<readonly ["posts", "bySlug", string], DefaultTypes>.queryKey: readonly ["posts", "bySlug", string]

A plain array, usually built by a group(). Handles keep its literal type.

queryKey
: posts<readonly ["bySlug", string]>(rest_0: "bySlug", rest_1: string): readonly ["posts", "bySlug", string]posts("bySlug",
input: {
    slug: string;
}
input
.slug: stringslug),
queryFn: (context: QueryFunctionContext<readonly ["posts", "bySlug", string]>) => Awaitable<Post>queryFn: () => function getPost(slug: string): Promise<Post>getPost(
input: {
    slug: string;
}
input
.slug: stringslug),
BaseOptions<readonly ["posts", "bySlug", string], DefaultTypes>.meta?: NoInfer<Partial<Readonly<Record<string, unknown>>>> | undefined

Settings for the root's links and features, and data for targets: the fields the root's parts declare, such as retry with the retry() link.

meta
: { retry: numberretry: 5 }, // or `false`
}); }

The retry() link declares the field, so a factory built with a bound queryOptions gets it typed only when the root has the link: see Query meta. On a client without the link, meta.retry does nothing.

Options

retry({ attempts, delay, when }) sets the defaults for every query of the client:

OptionDefault
attempts3: retries after the first attempt
delayDoubling from 1 second, at most 30 seconds
whenEvery error but a thrown Response: whether an error is worth retrying

delay(attempt) gets the retry's index, from 0. when(error) is the place to skip errors that won't go away:

const 
const patient: QueryClient<{
    readonly meta: Flat<Present<RetryMeta>>;
}>
patient
=
createQueryClient<readonly [Link<{
    readonly meta: RetryMeta;
}>], []>(config?: QueryClientConfig & {
    readonly features?: [] | undefined;
    readonly links?: readonly [Link<{
        readonly meta: RetryMeta;
    }>] | undefined;
}): QueryClient<{
    readonly meta: Flat<Present<RetryMeta>>;
}>

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
({
links?: (readonly AnyLink[] & readonly [Link<{
    readonly meta: RetryMeta;
}>]) | undefined

Wrap every request, outermost first.

links
: [
function retry(options?: RetryOptions): Link<{
    readonly meta: RetryMeta;
}>

Retry failed requests. Thrown Responses (router redirects and not-founds) and cancelled requests aren't retried, server clients never retry, and a browser that is offline waits until it's back online before retrying. A query sets its own attempts with meta: { retry }.

retry
({
RetryOptions.attempts?: number | undefined

Retries after the first attempt. A query's meta.retry wins. Default 3.

attempts
: 5,
RetryOptions.delay?: ((attempt: number) => number) | undefined

Wait before retry n (0-based), in ms. Default: doubling from 1 s, at most 30 s.

delay
: (attempt: numberattempt) => 500 * (attempt: numberattempt + 1),
RetryOptions.when?: ((error: unknown) => boolean) | undefined

Whether an error is worth retrying. Default: every error but a thrown Response.

when
: (error: unknownerror) => !(error: unknownerror instanceof class HttpErrorHttpError && error: HttpErrorerror.HttpError.status: numberstatus === 404),
}), ], });

Retries while offline

In the browser, a retry waits while navigator.onLine is false: after the delay, it waits for the window's online event. The query counts as fetching meanwhile. The first attempt always runs.

Timeouts

timeout(ms) is another link: it fails a request that takes longer than ms with a QueryTimeoutError, and aborts its signal. Links wrap each other in order, outermost first, so where it goes decides what it times:

// Each attempt gets 10 seconds.
const 
const perAttempt: QueryClient<{
    readonly meta: Flat<Present<RetryMeta> & Present<object>>;
}>
perAttempt
=
createQueryClient<readonly [Link<{
    readonly meta: RetryMeta;
}>, Link], []>(config?: QueryClientConfig & {
    readonly features?: [] | undefined;
    readonly links?: readonly [Link<{
        readonly meta: RetryMeta;
    }>, Link] | undefined;
}): QueryClient<{
    readonly meta: Flat<Present<RetryMeta> & Present<object>>;
}>

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
({
links?: (readonly AnyLink[] & readonly [Link<{
    readonly meta: RetryMeta;
}>, Link]) | undefined

Wrap every request, outermost first.

links
: [
function retry(options?: RetryOptions): Link<{
    readonly meta: RetryMeta;
}>

Retry failed requests. Thrown Responses (router redirects and not-founds) and cancelled requests aren't retried, server clients never retry, and a browser that is offline waits until it's back online before retrying. A query sets its own attempts with meta: { retry }.

retry
(), function timeout(ms: number): Link

Fail a request (and abort its signal) when it takes longer than ms.

timeout
(10_000)] });
// The request and all its retries get 30 seconds. const
const overall: QueryClient<{
    readonly meta: Flat<Present<RetryMeta> & Present<object>>;
}>
overall
=
createQueryClient<readonly [Link, Link<{
    readonly meta: RetryMeta;
}>], []>(config?: QueryClientConfig & {
    readonly features?: [] | undefined;
    readonly links?: readonly [Link, Link<{
        readonly meta: RetryMeta;
    }>] | undefined;
}): QueryClient<{
    readonly meta: Flat<Present<RetryMeta> & Present<object>>;
}>

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
({
links?: (readonly AnyLink[] & readonly [Link, Link<{
    readonly meta: RetryMeta;
}>]) | undefined

Wrap every request, outermost first.

links
: [function timeout(ms: number): Link

Fail a request (and abort its signal) when it takes longer than ms.

timeout
(30_000),
function retry(options?: RetryOptions): Link<{
    readonly meta: RetryMeta;
}>

Retry failed requests. Thrown Responses (router redirects and not-founds) and cancelled requests aren't retried, server clients never retry, and a browser that is offline waits until it's back online before retrying. A query sets its own attempts with meta: { retry }.

retry
()] });

Showing errors

What the user sees depends on whether there was data before the failure.

The first load fails. Reading the query throws to the nearest error boundary. Before the boundary renders its children again, start the query again the way the loader did, so the reader suspends on the new request instead of throwing the old error:

function function CommentsSection(): React.JSX.ElementCommentsSection() {
	return (
		<class CatchBoundaryCatchBoundary
			getResetKey: () => unknowngetResetKey={() => 
const loaderData: {
    comments: QueryHandle<typeof commentsQuery>;
}
loaderData
.
comments: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
comments
.
QueryHandle<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>.input: {
    slug: string;
}
input
.slug: stringslug}
errorComponent?: ErrorRouteComponent | undefinederrorComponent={function CommentsError(props: ErrorComponentProps): React.JSX.ElementCommentsError} > <const Suspense: React.ExoticComponent<React.SuspenseProps>

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

Suspense
React.SuspenseProps.fallback?: ReactNode

A fallback react tree to show when a Suspense child (like React.lazy) suspends

fallback
={<function CommentsSkeleton(): ReactNodeCommentsSkeleton />}>
<
function Comments(props: {
    readonly handle: QueryHandle<typeof commentsQuery>;
}): ReactNode
Comments
handle: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
handle
={
const loaderData: {
    comments: QueryHandle<typeof commentsQuery>;
}
loaderData
.
comments: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
comments
} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>

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

Suspense
>
</class CatchBoundaryCatchBoundary> ); } function function CommentsError(props: ErrorComponentProps): React.JSX.ElementCommentsError(props: ErrorComponentPropsprops:
type ErrorComponentProps<TError = unknown> = {
    error: TError;
    info?: {
        componentStack: string;
    };
    reset: () => void;
}
ErrorComponentProps
) {
const const queryClient: QueryClient<any>queryClient = function useQueryClient(): QueryClient

The client from the nearest QueryClientProvider.

useQueryClient
();
return ( <React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div React.HTMLAttributes<HTMLDivElement>.role?: React.AriaRole | undefinedrole="alert"> <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p> Couldn't load comments:{" "} {props: ErrorComponentPropsprops.error: unknownerror instanceof var Error: ErrorConstructorError ? props: ErrorComponentPropsprops.error: Errorerror.Error.message: stringmessage :
var String: StringConstructor
(value?: any) => string

Allows manipulation and formatting of text strings and determination and location of substrings within strings.

String
(props: ErrorComponentPropsprops.error: unknownerror)}
</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p> <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() => { void
load<{
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
}>(client: QueryClient, entries: {
    readonly comments: LoadEntry<(input: {
        slug: string;
    }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">;
}): Promise<...>

Start a loader's queries and return a serializable handle for each. It starts every entry before it waits for any, so the order of the entries can't create a waterfall. Resolves with [handles, data] once every swr and revalidate entry has data, and rejects with the error as soon as one fails. prefetch entries never make it reject. A skipToken in place of an entry starts nothing and gets no handle.

load
(const queryClient: QueryClient<any>queryClient, {
comments: LoadEntry<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, "prefetch">
comments
:
prefetch<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>(factory: (input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>, ...args: LoadEntryArgs<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>): LoadEntry<...>

Start the query if the cache has no fresh data, and don't wait for it. It never makes the load reject: errors show up where a component reads the handle.

prefetch
(
function commentsQuery(input: {
    slug: string;
}): InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>
commentsQuery
,
const loaderData: {
    comments: QueryHandle<typeof commentsQuery>;
}
loaderData
.
comments: QueryHandle<(input: {
    slug: string;
}) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>
comments
.
QueryHandle<(input: { slug: string; }) => InfiniteQueryOptions<readonly ["posts", "comments", string], CommentPage, number, InfiniteData<CommentPage, number>, DefaultTypes>>.input: {
    slug: string;
}
input
),
}); props: ErrorComponentPropsprops.reset: () => voidreset(); }} > Try again </React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button> </React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div> ); }

Any error boundary works: CatchBoundary from TanStack Router, react-error-boundary, Next.js's error.tsx, or your own class component.

A background refetch fails. The existing data stays on screen: losing it because a refetch failed would be worse than showing it a little longer. The error is on the reader's state as error, for a hint:

const [
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable
,
const controls: QueryState<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
controls
] =
useQuery<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>(factory: (input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>, handle: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>, options?: UseQueryOptions): readonly [...]

Read a query by its handle.

useQuery
(
function postQuery(input: {
    slug: string;
}): QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>
postQuery
,
const loaderData: {
    post: QueryHandle<typeof postQuery>;
}
loaderData
.
post: QueryHandle<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
post
);
const const post: Postpost = use<Post>(usable: React.Usable<Post>): Postuse(
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable
);
{
const controls: QueryState<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
controls
.QueryState<(input: { slug: string; }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>.error: unknown

The last request's error while it's the latest outcome.

error
? (
<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p React.HTMLAttributes<HTMLParagraphElement>.role?: React.AriaRole | undefinedrole="status"> Couldn't refresh. Showing data from {function formatTime(value: number): stringformatTime(
const controls: QueryState<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
controls
.QueryState<(input: { slug: string; }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>.updatedAt: number

When the data was fetched (ms since epoch); 0 without data.

updatedAt
)}.
</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p> ) : null; }

Make the next request fail in the demo, then reload a post to see each case:

Errors from loaders

A failed prefetch never makes load reject: the error surfaces where the data is read, and only that part of the page shows it. A failed swr or revalidate makes load reject, so it reaches the route's error boundary (errorComponent in TanStack Router, error.tsx in Next.js), replacing the page.

This is one more reason to choose per query: wait with swr for what the page can't do without, and prefetch the rest, so a failing sidebar doesn't take the whole page down.

One place for every error

A link sees every request of the client, so it's the place for logging and toasts. Put it before retry() to see each request's final error, after its retries. It can declare a meta field of its own, for a per-query message:

const 
const reportErrors: Link<{
    meta: {
        errorMessage?: string;
    };
}>
reportErrors
=
link<{
    meta: {
        errorMessage?: string;
    };
}>(run: (op: Operation<Present<{
    errorMessage?: string;
}> & object>, next: (op: Operation) => Promise<unknown>) => Promise<unknown>): Link<{
    meta: {
        errorMessage?: string;
    };
}>

A link that declares the meta fields it reads. A root's queries take those fields only when the root has the link.

link
<{
meta: {
    errorMessage?: string;
}
meta
: { errorMessage?: string | undefinederrorMessage?: string } }>(
async (
op: Operation<Present<{
    errorMessage?: string;
}> & object>
op
, next: (op: Operation) => Promise<unknown>next) => {
try { return await next: (op: Operation) => Promise<unknown>next(
op: Operation<Present<{
    errorMessage?: string;
}> & object>
op
);
} catch (function (local var) error: unknownerror) { if (!
op: Operation<Present<{
    errorMessage?: string;
}> & object>
op
.Operation<TMeta extends object = object>.signal: AbortSignalsignal.AbortSignal.aborted: boolean

The aborted read-only property returns a value that indicates whether the asynchronous operations the signal is communicating with are aborted (true) or not (false).

aborted
) {
function reportError(error: unknown, context: {
    readonly queryKey: unknown;
}): void
reportError
(function (local var) error: unknownerror, { queryKey: unknownqueryKey:
op: Operation<Present<{
    errorMessage?: string;
}> & object>
op
.Operation<TMeta extends object = object>.queryKey: QueryKeyqueryKey });
function showToast(message: string): voidshowToast(
op: Operation<Present<{
    errorMessage?: string;
}> & object>
op
.
Operation<Present<{ errorMessage?: string; }> & object>.meta: Present<{
    errorMessage?: string;
}> & object

The query's meta, {} when it has none.

meta
.errorMessage: string | undefinederrorMessage ?? "Something went wrong");
} throw function (local var) error: unknownerror; } }, ); const
const reporting: QueryClient<{
    readonly meta: Flat<Present<{
        errorMessage?: string;
    }> & Present<RetryMeta>>;
}>
reporting
=
createQueryClient<readonly [Link<{
    meta: {
        errorMessage?: string;
    };
}>, Link<{
    readonly meta: RetryMeta;
}>], []>(config?: QueryClientConfig & {
    readonly features?: [] | undefined;
    readonly links?: readonly [Link<{
        meta: {
            errorMessage?: string;
        };
    }>, Link<{
        readonly meta: RetryMeta;
    }>] | undefined;
}): QueryClient<{
    readonly meta: Flat<Present<{
        errorMessage?: string;
    }> & Present<RetryMeta>>;
}>

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
({
links?: (readonly AnyLink[] & readonly [Link<{
    meta: {
        errorMessage?: string;
    };
}>, Link<{
    readonly meta: RetryMeta;
}>]) | undefined

Wrap every request, outermost first.

links
: [
const reportErrors: Link<{
    meta: {
        errorMessage?: string;
    };
}>
reportErrors
,
function retry(options?: RetryOptions): Link<{
    readonly meta: RetryMeta;
}>

Retry failed requests. Thrown Responses (router redirects and not-founds) and cancelled requests aren't retried, server clients never retry, and a browser that is offline waits until it's back online before retrying. A query sets its own attempts with meta: { retry }.

retry
()] });

Redirects

In TanStack Start, a queryFn can throw the router's redirect(), for instance when a session has expired. setupRouterIntegration turns it into a navigation wherever the query failed: a loader, a render or a background refetch.

A redirect is a thrown Response, which retry() doesn't retry, so it navigates at once. A when of your own replaces that check, so keep it:

const 
const withRedirects: QueryClient<{
    readonly meta: Flat<Present<RetryMeta>>;
}>
withRedirects
=
createQueryClient<readonly [Link<{
    readonly meta: RetryMeta;
}>], []>(config?: QueryClientConfig & {
    readonly features?: [] | undefined;
    readonly links?: readonly [Link<{
        readonly meta: RetryMeta;
    }>] | undefined;
}): QueryClient<{
    readonly meta: Flat<Present<RetryMeta>>;
}>

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
({
links?: (readonly AnyLink[] & readonly [Link<{
    readonly meta: RetryMeta;
}>]) | undefined

Wrap every request, outermost first.

links
: [
function retry(options?: RetryOptions): Link<{
    readonly meta: RetryMeta;
}>

Retry failed requests. Thrown Responses (router redirects and not-founds) and cancelled requests aren't retried, server clients never retry, and a browser that is offline waits until it's back online before retrying. A query sets its own attempts with meta: { retry }.

retry
({
RetryOptions.when?: ((error: unknown) => boolean) | undefined

Whether an error is worth retrying. Default: every error but a thrown Response.

when
: (error: unknownerror) =>
!function isRedirect(obj: any): obj is AnyRedirect

Check whether a value is a TanStack Router redirect Response.

isRedirect
(error: unknownerror) &&
!(error: unknownerror instanceof class HttpErrorHttpError && error: HttpErrorerror.HttpError.status: numberstatus === 404), }), ], });