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:
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.
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" | undefinedWhile 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: booleanShowing 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.
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" | undefinedThe 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.
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.
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" | undefinedThe 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.
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: booleanShowing 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:
whilePending | While the new input loads | Use 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 arrives | Short 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 lands | Lists, 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?: ReactNodeA 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" | undefinedWhile 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: stringThe 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: booleanShowing 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 withuseInfiniteQueryand extended withfetchNextPage. 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.