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

URL state and pagination

Put filters, sort and page in the URL, let the loader turn them into the query's input, and pick what shows while the new input loads with whilePending.

Filters, sort order and pagination are search params. The loader turns them into the query's input, so every navigation starts the right query and the component only ever reads the handle it was given:

src/routes/posts.tsx
import { import zz } from "zod";

const 
const PostsSearch: z.ZodObject<{
    sort: z.ZodDefault<z.ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: z.ZodDefault<z.ZodInt>;
}, z.core.$strip>
PostsSearch
= import zz.
function object<{
    sort: z.ZodDefault<z.ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: z.ZodDefault<z.ZodInt>;
}>(shape?: {
    sort: z.ZodDefault<z.ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: z.ZodDefault<z.ZodInt>;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    sort: z.ZodDefault<z.ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: z.ZodDefault<...>;
}, z.core.$strip>
object
({
sort: z.ZodDefault<z.ZodEnum<{
    newest: "newest";
    oldest: "oldest";
}>>
sort
: import zz.
enum<readonly ["newest", "oldest"]>(values: readonly ["newest", "oldest"], params?: string | z.core.$ZodEnumParams): z.ZodEnum<{
    newest: "newest";
    oldest: "oldest";
}> (+1 overload)
export enum
enum
(["newest", "oldest"]).
ZodType<any, any, $ZodEnumInternals<{ newest: "newest"; oldest: "oldest"; }>>.default(def: "newest" | "oldest"): z.ZodDefault<z.ZodEnum<{
    newest: "newest";
    oldest: "oldest";
}>> (+1 overload)
default
("newest"),
page: z.ZodDefault<z.ZodInt>page: import zz.function int(params?: string | z.core.$ZodCheckNumberFormatParams): z.ZodIntint()._ZodNumber<$ZodNumberInternals<number>>.positive(params?: string | z.core.$ZodCheckGreaterThanParams): z.ZodIntpositive().ZodType<any, any, $ZodNumberInternals<number>>.default(def: number): z.ZodDefault<z.ZodInt> (+1 overload)default(1), }); export const
const Route: Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", "/posts", "/posts", "/posts", z.ZodObject<{
    sort: z.ZodDefault<z.ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: z.ZodDefault<z.ZodInt>;
}, z.core.$strip>, ResolveParams<"/posts">, AnyContext, AnyContext, AnyContext, {
    sort: "newest" | "oldest";
    page: number;
}, ... 5 more ..., undefined>
Route
=
createFileRoute<"/posts", RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", "/posts", "/posts">(path?: "/posts" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>

Creates a file-based Route factory for a given path.

@parampath — File path literal for the route (usually auto-generated).@returnsA function that accepts Route options and returns a Route instance.
createFileRoute
("/posts")({
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 12 more ..., undefined>.validateSearch?: Constrain<z.ZodObject<{
    sort: z.ZodDefault<z.ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: z.ZodDefault<z.ZodInt>;
}, z.core.$strip>, AnyValidator, DefaultValidator>
validateSearch
:
const PostsSearch: z.ZodObject<{
    sort: z.ZodDefault<z.ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: z.ZodDefault<z.ZodInt>;
}, z.core.$strip>
PostsSearch
,
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 12 more ..., undefined>.loaderDeps?: ((opts: FullSearchSchemaOption<RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, z.ZodObject<{
    sort: z.ZodDefault<z.ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: z.ZodDefault<z.ZodInt>;
}, z.core.$strip>>) => {
    sort: "newest" | "oldest";
    page: number;
}) | undefined
loaderDeps
: (
args: FullSearchSchemaOption<RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, z.ZodObject<{
    sort: z.ZodDefault<z.ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: z.ZodDefault<z.ZodInt>;
}, z.core.$strip>>
args
) =>
args: FullSearchSchemaOption<RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, z.ZodObject<{
    sort: z.ZodDefault<z.ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: z.ZodDefault<z.ZodInt>;
}, z.core.$strip>>
args
.
FullSearchSchemaOption<RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, ZodObject<...>>.search: {
    sort: "newest" | "oldest";
    page: number;
}
search
,
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, {}, ... 5 more ..., undefined>, ... 12 more ..., undefined>.loader?: Constrain<(args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", ResolveParams<"/posts">, {
    sort: "newest" | "oldest";
    page: number;
}, AnyContext, AnyContext, AnyContext, unknown, undefined>) => Promise<{
    readonly posts: QueryHandle<(input: {
        page: number;
        sort: "newest" | "oldest";
    }) => QueryOptions<readonly ["posts", "list", {
        ...;
    }], readonly Post[], readonly Post[], DefaultTypes>>;
}>, RouteLoaderFn<...> | RouteLoaderObject<...>> | undefined
loader
: async (
args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", ResolveParams<"/posts">, {
    sort: "newest" | "oldest";
    page: number;
}, ... 4 more ..., undefined>
args
) => {
const [
const handles: {
    readonly posts: QueryHandle<(input: {
        page: number;
        sort: "newest" | "oldest";
    }) => QueryOptions<readonly ["posts", "list", {
        page: number;
        sort: "newest" | "oldest";
    }], readonly Post[], readonly Post[], DefaultTypes>>;
}
handles
] = await
load<{
    readonly posts: LoadEntry<(input: {
        page: number;
        sort: "newest" | "oldest";
    }) => QueryOptions<readonly ["posts", "list", {
        page: number;
        sort: "newest" | "oldest";
    }], readonly Post[], readonly Post[], DefaultTypes>, "prefetch">;
}>(client: QueryClient, entries: {
    readonly posts: LoadEntry<(input: {
        page: number;
        sort: "newest" | "oldest";
    }) => QueryOptions<readonly ["posts", "list", {
        page: number;
        sort: "newest" | "oldest";
    }], readonly Post[], readonly Post[], DefaultTypes>, "prefetch">;
}): Promise<...>

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

load
(
args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", ResolveParams<"/posts">, {
    sort: "newest" | "oldest";
    page: number;
}, ... 4 more ..., undefined>
args
.
LoaderFnContext<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 7 more ..., undefined>.context: {
    queryClient: QueryClient;
}
context
.queryClient: QueryClient<any>queryClient, {
posts: LoadEntry<(input: {
    page: number;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["posts", "list", {
    page: number;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>, "prefetch">
posts
:
prefetch<(input: {
    page: number;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["posts", "list", {
    page: number;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>(factory: (input: {
    page: number;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["posts", "list", {
    page: number;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>, ...args: LoadEntryArgs<(input: {
    page: number;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["posts", "list", {
    page: number;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], 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 postsQuery(input: {
    page: number;
    sort: "newest" | "oldest";
}): QueryOptions<readonly ["posts", "list", {
    page: number;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>
postsQuery
,
args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", ResolveParams<"/posts">, {
    sort: "newest" | "oldest";
    page: number;
}, ... 4 more ..., undefined>
args
.
LoaderFnContext<Register, RootRoute<Register, undefined, { queryClient: QueryClient; }, AnyContext, AnyContext, {}, ... 5 more ..., undefined>, ... 7 more ..., undefined>.deps: {
    sort: "newest" | "oldest";
    page: number;
}
deps
),
}); return
const handles: {
    readonly posts: QueryHandle<(input: {
        page: number;
        sort: "newest" | "oldest";
    }) => QueryOptions<readonly ["posts", "list", {
        page: number;
        sort: "newest" | "oldest";
    }], readonly Post[], readonly Post[], DefaultTypes>>;
}
handles
;
}, UpdatableRouteOptionsExtensions.component?: RouteComponent | undefinedcomponent: function PostsPage(): React.JSX.ElementPostsPage, }); function function PostsPage(): React.JSX.ElementPostsPage() { const
const loaderData: {
    readonly posts: QueryHandle<(input: {
        page: number;
        sort: "newest" | "oldest";
    }) => QueryOptions<readonly ["posts", "list", {
        page: number;
        sort: "newest" | "oldest";
    }], readonly Post[], readonly Post[], DefaultTypes>>;
}
loaderData
=
const Route: Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", "/posts", "/posts", "/posts", z.ZodObject<{
    sort: z.ZodDefault<z.ZodEnum<{
        newest: "newest";
        oldest: "oldest";
    }>>;
    page: z.ZodDefault<z.ZodInt>;
}, z.core.$strip>, ResolveParams<"/posts">, AnyContext, AnyContext, AnyContext, {
    sort: "newest" | "oldest";
    page: number;
}, ... 5 more ..., undefined>
Route
.
RouteExtensions<"/posts", "/posts">.useLoaderData: <RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>, unknown, boolean>(opts?: (UseLoaderDataBaseOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, ... 8 more ..., undefined>, "never", false, RouterHistory, Record<...>>, "/posts", true, unknown, boolean> & OptionalStructuralSharing<...>) | undefined) => {
    ...;
}
useLoaderData
();
const [
const postsThenable: QueryValue<(input: {
    page: number;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["posts", "list", {
    page: number;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>
postsThenable
,
const postsState: QueryState<(input: {
    page: number;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["posts", "list", {
    page: number;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>
postsState
] =
useQuery<(input: {
    page: number;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["posts", "list", {
    page: number;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>(factory: (input: {
    page: number;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["posts", "list", {
    page: number;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>, handle: QueryHandle<(input: {
    page: number;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["posts", "list", {
    page: number;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>, options?: UseQueryOptions): readonly [...]

Read a query by its handle.

useQuery
(
function postsQuery(input: {
    page: number;
    sort: "newest" | "oldest";
}): QueryOptions<readonly ["posts", "list", {
    page: number;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>
postsQuery
,
const loaderData: {
    readonly posts: QueryHandle<(input: {
        page: number;
        sort: "newest" | "oldest";
    }) => QueryOptions<readonly ["posts", "list", {
        page: number;
        sort: "newest" | "oldest";
    }], readonly Post[], readonly Post[], DefaultTypes>>;
}
loaderData
.
posts: QueryHandle<(input: {
    page: number;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["posts", "list", {
    page: number;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>
posts
, {
UseQueryOptions.whilePending?: "previous" | "suspend" | undefined

While a new handle's query has no data: "suspend" (the default) shows the Suspense fallback; "previous" keeps showing the last handle's data and sets isPrevious.

whilePending
: "previous",
}); const const posts: readonly Post[]posts = use<readonly Post[]>(usable: React.Usable<readonly Post[]>): readonly Post[]use(
const postsThenable: QueryValue<(input: {
    page: number;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["posts", "list", {
    page: number;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>
postsThenable
);
return ( <> <React.JSX.IntrinsicElements.nav: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>nav React.AriaAttributes["aria-busy"]?: Booleanish | undefinedaria-busy={
const postsState: QueryState<(input: {
    page: number;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["posts", "list", {
    page: number;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>
postsState
.QueryState<(input: { page: number; sort: "newest" | "oldest"; }) => QueryOptions<readonly ["posts", "list", { page: number; sort: "newest" | "oldest"; }], readonly Post[], readonly Post[], DefaultTypes>>.isPrevious: boolean

Showing the previous handle's data (whilePending: "previous").

isPrevious
}>
<const Link: LinkComponent<"a", string>

A strongly-typed anchor component for declarative navigation. Handles path, search, hash and state updates with optional route preloading and active-state styling.

@returnsAn anchor-like element that navigates without full page reloads.
Link
OptionalToOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, "never", false, RouterHistory, Record<...>>, string, ".">.to?: "/posts" | "/login" | "/settings" | "/blog/$slug" | "/posts/$slug" | "/" | "." | ".." | "/dashboard" | undefined

The internal route path to navigate to. This should be a relative or absolute path within your application. For external URLs, use the href property instead.

to
="."
MakeOptionalSearchParams<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, "never", false, RouterHistory, Record<...>>, string, ".">.search?: true | {
    sort?: "newest" | "oldest" | undefined;
    page?: number | undefined;
} | ParamsReducerFn<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>, "SEARCH", string, "."> | undefined
search
={(
prev: {
    sort?: "newest" | "oldest" | undefined;
    page?: number | undefined;
}
prev
) => ({ ...
prev: {
    sort?: "newest" | "oldest" | undefined;
    page?: number | undefined;
}
prev
, sort?: "newest" | "oldest" | undefinedsort: "newest" })}>
Newest </const Link: LinkComponent<"a", string>

A strongly-typed anchor component for declarative navigation. Handles path, search, hash and state updates with optional route preloading and active-state styling.

@returnsAn anchor-like element that navigates without full page reloads.
Link
>
<const Link: LinkComponent<"a", string>

A strongly-typed anchor component for declarative navigation. Handles path, search, hash and state updates with optional route preloading and active-state styling.

@returnsAn anchor-like element that navigates without full page reloads.
Link
OptionalToOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, "never", false, RouterHistory, Record<...>>, string, ".">.to?: "/posts" | "/login" | "/settings" | "/blog/$slug" | "/posts/$slug" | "/" | "." | ".." | "/dashboard" | undefined

The internal route path to navigate to. This should be a relative or absolute path within your application. For external URLs, use the href property instead.

to
="."
MakeOptionalSearchParams<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, { queryClient: QueryClient; }, AnyContext, AnyContext, ... 6 more ..., undefined>, "never", false, RouterHistory, Record<...>>, string, ".">.search?: true | {
    sort?: "newest" | "oldest" | undefined;
    page?: number | undefined;
} | ParamsReducerFn<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>, "SEARCH", string, "."> | undefined
search
={(
prev: {
    sort?: "newest" | "oldest" | undefined;
    page?: number | undefined;
}
prev
) => ({ ...
prev: {
    sort?: "newest" | "oldest" | undefined;
    page?: number | undefined;
}
prev
, sort?: "newest" | "oldest" | undefinedsort: "oldest" })}>
Oldest </const Link: LinkComponent<"a", string>

A strongly-typed anchor component for declarative navigation. Handles path, search, hash and state updates with optional route preloading and active-state styling.

@returnsAn anchor-like element that navigates without full page reloads.
Link
>
</React.JSX.IntrinsicElements.nav: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>nav> <
function PostList(props: {
    readonly dimmed: boolean;
    readonly posts: ReadonlyArray<Post>;
}): React.JSX.Element
PostList
posts: readonly Post[]posts={const posts: readonly Post[]posts} dimmed: booleandimmed={
const postsState: QueryState<(input: {
    page: number;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["posts", "list", {
    page: number;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>
postsState
.QueryState<(input: { page: number; sort: "newest" | "oldest"; }) => QueryOptions<readonly ["posts", "list", { page: number; sort: "newest" | "oldest"; }], readonly Post[], readonly Post[], DefaultTypes>>.isPrevious: boolean

Showing the previous handle's data (whilePending: "previous").

isPrevious
} />
</> ); }

Clicking a link navigates, the loader starts { sort: "oldest", page: 1 } with prefetch without blocking, and the component receives a new handle.

What shows while the new input loads

That's up to useQuery's whilePending option:

whilePendingWhile the new input loadsUse for
"suspend" (default)use() suspends. TanStack Router and Next.js run navigations as React transitions, so React keeps the whole previous screen, including the old URL's active links, until the data arrivesShort loads, and pages that shouldn't show stale data
"previous"The navigation commits immediately. useQuery keeps returning the previous input's data, and isPrevious is true on its state until the new data landsLists, filters, pagination: anything that should feel instant

Either way, an input that's already cached renders immediately. A component that mounts with a pending handle has no previous data, so "previous" suspends like "suspend" on first load.

Page through the demo with each mode. Watch the address bar: with "suspend" it waits for the data, with "previous" it updates at once and the old page dims. Hovering Next prefetches the next page, so with a bit of patience neither mode waits.

"previous" covers what TanStack Query's placeholderData: keepPreviousData and isPlaceholderData do.

Skeletons for unrelated content

To show a skeleton instead, for instance when moving between unrelated posts, give the Suspense boundary the query's key. React treats it as new content and shows its fallback, even inside a transition:

// This loader starts the post with `prefetch`.
const [
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable
,
const postState: QueryState<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postState
] =
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
);
return ( <const Suspense: React.ExoticComponent<React.SuspenseProps>

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

Suspense
React.Attributes.key?: React.Key | null | undefinedkey={
const postState: QueryState<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postState
.QueryState<(input: { slug: string; }) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>.hash: stringhash} React.SuspenseProps.fallback?: ReactNode

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

fallback
={<function PostSkeleton(): ReactNodePostSkeleton />}>
<
function PostBody(props: {
    readonly post: Promise<Post>;
}): ReactNode
PostBody
post: Promise<Post>post={
const postThenable: QueryValue<(input: {
    slug: string;
}) => QueryOptions<readonly ["posts", "bySlug", string], Post, Post, DefaultTypes>>
postThenable
} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>

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

Suspense
>
);

hash on the state is the stable string form of the query's key.

Input typed before it reaches the URL

A search box changes the input on every keystroke, faster than navigations should run. Keep what's being typed in state, and derive the reader's handle from the loader's with waterfallHandle(factory, handle, update). update gets the handle's input and returns the new one:

// `props.search` is the handle the loader started from the URL's `?q=`.
function 
function Search(props: {
    search: QueryHandle<typeof searchQuery>;
}): React.JSX.Element
Search
(
props: {
    search: QueryHandle<typeof searchQuery>;
}
props
: {
search: QueryHandle<(input: {
    q: string;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>
search
: 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 searchQuery(input: {
    q: string;
    sort: "newest" | "oldest";
}): QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>
searchQuery
> }) {
const [const q: stringq, const setQ: React.Dispatch<React.SetStateAction<string>>setQ] = useState<string>(initialState: string | (() => string)): [string, React.Dispatch<React.SetStateAction<string>>] (+1 overload)

Returns a stateful value, and a function to update it.

useState
(
props: {
    search: QueryHandle<typeof searchQuery>;
}
props
.
search: QueryHandle<(input: {
    q: string;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>
search
.
QueryHandle<(input: { q: string; sort: "newest" | "oldest"; }) => QueryOptions<readonly ["search", { q: string; sort: "newest" | "oldest"; }], readonly Post[], readonly Post[], DefaultTypes>>.input: {
    q: string;
    sort: "newest" | "oldest";
}
input
.q: stringq);
const
const searchHandle: QueryHandle<(input: {
    q: string;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>
searchHandle
=
waterfallHandle<(input: {
    q: string;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>(factory: (input: {
    q: string;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>, handle: QueryHandle<(input: {
    q: string;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>, update: (input: {
    q: string;
    sort: "newest" | "oldest";
}) => {
    q: string;
    sort: "newest" | "oldest";
}): QueryHandle<...> (+1 overload)

The handle of a query whose input is only known while rendering, like an id in another query's data. It starts nothing: the component that reads it fetches when it renders, which makes it a request waterfall, and the name keeps that visible. Prefer starting queries in a loader with load.

waterfallHandle
(
function searchQuery(input: {
    q: string;
    sort: "newest" | "oldest";
}): QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>
searchQuery
,
props: {
    search: QueryHandle<typeof searchQuery>;
}
props
.
search: QueryHandle<(input: {
    q: string;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>
search
, (
input: {
    q: string;
    sort: "newest" | "oldest";
}
input
) => ({
...
input: {
    q: string;
    sort: "newest" | "oldest";
}
input
,
q: stringq, })); const [
const resultsThenable: QueryValue<(input: {
    q: string;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>
resultsThenable
,
const resultsState: QueryState<(input: {
    q: string;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>
resultsState
] =
useQuery<(input: {
    q: string;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>(factory: (input: {
    q: string;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>, handle: QueryHandle<(input: {
    q: string;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>, options?: UseQueryOptions): readonly [...]

Read a query by its handle.

useQuery
(
function searchQuery(input: {
    q: string;
    sort: "newest" | "oldest";
}): QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>
searchQuery
,
const searchHandle: QueryHandle<(input: {
    q: string;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>
searchHandle
, {
UseQueryOptions.whilePending?: "previous" | "suspend" | undefined

While a new handle's query has no data: "suspend" (the default) shows the Suspense fallback; "previous" keeps showing the last handle's data and sets isPrevious.

whilePending
: "previous",
}); const const results: readonly Post[]results = use<readonly Post[]>(usable: React.Usable<readonly Post[]>): readonly Post[]use(
const resultsThenable: QueryValue<(input: {
    q: string;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>
resultsThenable
);
return ( <> <React.JSX.IntrinsicElements.input: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>input React.InputHTMLAttributes<HTMLInputElement>.value?: string | number | readonly string[] | undefinedvalue={const q: stringq} React.InputHTMLAttributes<HTMLInputElement>.onChange?: React.ChangeEventHandler<HTMLInputElement, HTMLInputElement> | undefinedonChange={(event: React.ChangeEvent<HTMLInputElement, HTMLInputElement>event) => const setQ: (value: React.SetStateAction<string>) => voidsetQ(event: React.ChangeEvent<HTMLInputElement, HTMLInputElement>event.React.ChangeEvent<HTMLInputElement, HTMLInputElement>.target: EventTarget & HTMLInputElementtarget.HTMLInputElement.value: string

The value property of the HTMLInputElement interface represents the current value of the element as a string.

value
)} />
<
function Results(props: {
    readonly dimmed: boolean;
    readonly posts: ReadonlyArray<Post>;
}): ReactNode
Results
posts: readonly Post[]posts={const results: readonly Post[]results} dimmed: booleandimmed={
const resultsState: QueryState<(input: {
    q: string;
    sort: "newest" | "oldest";
}) => QueryOptions<readonly ["search", {
    q: string;
    sort: "newest" | "oldest";
}], readonly Post[], readonly Post[], DefaultTypes>>
resultsState
.QueryState<(input: { q: string; sort: "newest" | "oldest"; }) => QueryOptions<readonly ["search", { q: string; sort: "newest" | "oldest"; }], readonly Post[], readonly Post[], DefaultTypes>>.isPrevious: boolean

Showing the previous handle's data (whilePending: "previous").

isPrevious
} />
</> ); }

While q is still the URL's, the new input maps to the same query, so waterfallHandle returns the loader's handle itself and useQuery reads it as before. Each new q is a query no loader started: the component fetches it while rendering, a request waterfall the name keeps visible. Write the term to the URL when the user submits or pauses, so the next loader starts it and links share it.

Pagination versus infinite scroll

They differ in where the page lives:

  • Pagination: the page is a search param and part of the input, so each page is its own query and the URL links to it.
  • Infinite scroll: filters and sort are search params, and pages are loaded into one paged query per input with infiniteQueryOptions, read with useInfiniteQuery and extended with fetchNextPage. Changing a filter starts a new list from its first page. See Infinite queries.

Why not local state?

It's tempting to keep the page number in useState and build the input from it. Don't: the loader can't see component state, so the query would start only after render, the back button wouldn't restore the page, and a shared link would open page one. Keeping the input in the URL makes every view linkable and every query startable before render.

In Next.js the same rules apply: read searchParams in the page, start the query with them in load, and pass the handle to the client component.