Prefetching with handles
On this page
A handle is a small, serializable reference to a query. You load it in a server component, a route loader or an event handler. Pass it to a component, and the component reads the query, fully typed, without naming the procedure or repeating its input.
Take a post page. Its loader, or server component, lists the queries the page needs and loads them in one call:
import {
function ensureEntry<TDef extends ResolverDef>(procedure: DecorateQueryProcedure<TDef>, ...args: EntryArgs<TDef>): TRPCQueryEntry<TDef, "ensure">ensureEntry,
function loadHandles<TFeatureFlags extends FeatureFlags, TEntries extends Record<string, AnyTRPCEntry>>(trpc: DecorateRouterKeyable<TFeatureFlags>, entries: TEntries): Promise<TRPCLoadedHandles<TEntries>>loadHandles,
function prefetchInfiniteEntry<TDef extends ResolverDef>(procedure: DecorateInfiniteQueryProcedure<TDef>, input: TDef["input"], opts?: TRPCInfiniteEntryOptions<TDef>): TRPCInfiniteQueryEntry<TDef, "prefetch">prefetchInfiniteEntry,
} from '@trpc/tanstack-react-query/unstable-handles';
const [const handles: HandlesOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
handles, const data: DataOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
data] = await loadHandles<FeatureFlags, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>(trpc: DecorateRouterKeyable<...>, entries: {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}): Promise<...>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
// 1. The post: the load waits for it
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
post: ensureEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
ensureEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid }),
// 2. Its comments: start at the same time, and stream in later
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
comments: prefetchInfiniteEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
...;
}, opts?: TRPCInfiniteEntryOptions<...> | undefined): TRPCInfiniteQueryEntry<...>
prefetchInfiniteEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments, { postId: stringpostId: const id: stringid }),
});
const data: DataOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
data.post: Postpost.title: stringtitle;
// The comments were only prefetched, not awaited, so they aren't on `data`.
// Read them in the component with `fromHandle(trpc, handles.comments)`.
const data: DataOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
data.comments;data only has the entries the load waited for. The comments may have no data yet, so reading them is a type error, not a silent undefined. If the post is missing, the load rejects with an error that names the post entry: see Errors.
The page gets the handles as props, and reads them without naming a procedure or repeating an input:
import { function useSuspenseQuery<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = readonly unknown[]>(options: UseSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): UseSuspenseQueryResult<TData, TError>useSuspenseQuery } from '@tanstack/react-query';
import { function fromHandle<TDef extends ResolverDef, TData = TDef["output"]>(trpc: DecorateRouterKeyable<NoInfer<TDef["featureFlags"]>>, handle: TRPCQueryHandle<TDef>, opts?: UnusedSkipTokenTRPCQueryOptionsIn<NoInfer<TDef["output"]>, TData, ErrorOf<TDef>, TDef["featureFlags"]>): UnusedSkipTokenTRPCQueryOptionsOut<TDef["output"], TData, ErrorOf<TDef>, TDef["featureFlags"]> (+1 overload)fromHandle } from '@trpc/tanstack-react-query/unstable-handles';
import { const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense } from 'react';
import { function Comments(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
Comments } from '~/components/comments';
import { function MoreByAuthor(props: {
authorId: string;
}): JSX.Element
MoreByAuthor } from '~/components/more-by-author';
import { const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
useTRPC } from '~/trpc/client';
export function function PostPage(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
PostPage(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}
props: {
post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post: type inferQueryHandle<TProcedure extends DecorateQueryProcedure<any>> = TProcedure extends DecorateQueryProcedure<infer TDef extends ResolverDef> ? TRPCQueryHandle<TDef> : neverinferQueryHandle<type TRPC = DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>, {
...;
}> & DecorateRouterKeyable<...>
TRPC['post']['byId']>;
comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments: type inferInfiniteQueryHandle<TProcedure extends DecorateInfiniteQueryProcedure<any>> = TProcedure extends DecorateInfiniteQueryProcedure<infer TDef extends ResolverDef> ? TRPCInfiniteQueryHandle<TDef> : neverinferInfiniteQueryHandle<type TRPC = DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>, {
...;
}> & DecorateRouterKeyable<...>
TRPC['post']['comments']>;
}) {
const const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc = function useTRPC(): TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
useTRPC();
const const postQuery: UseSuspenseQueryResult<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>>
postQuery = useSuspenseQuery<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, Post, TRPCQueryKeyWithoutPrefix>(options: UseSuspenseQueryOptions<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, Post, TRPCQueryKeyWithoutPrefix>, queryClient?: QueryClient): UseSuspenseQueryResult<...>
useSuspenseQuery(fromHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, Post>(trpc: DecorateRouterKeyable<NoInfer<{
keyPrefix: false;
}>>, handle: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, opts?: UnusedSkipTokenTRPCQueryOptionsIn<NoInfer<Post>, Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, {
keyPrefix: false;
}> | undefined): UnusedSkipTokenTRPCQueryOptionsOut<...> (+1 overload)
fromHandle(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}
props.post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post));
return (
<React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article>
<React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const postQuery: UseSuspenseQueryResult<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>>
postQuery.data: Postdata.title: stringtitle}</React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>
<const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading comments…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}>
<function Comments(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
Comments comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments={props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}
props.comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense>
{/* 3. More by the author: ⚠️ a waterfall */}
<const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading more by this author…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}>
<function MoreByAuthor(props: {
authorId: string;
}): JSX.Element
MoreByAuthor authorId: stringauthorId={const postQuery: UseSuspenseQueryResult<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>>
postQuery.data: Postdata.authorId: stringauthorId} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense>
</React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article>
);
}Mutations don't change: use useTRPC()'s mutationOptions. To show a new comment, refresh the comments handle when the mutation succeeds:
import { function useMutation<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(options: UseMutationOptions<TData, TError, TVariables, TOnMutateResult>, queryClient?: QueryClient): UseMutationResult<TData, TError, TVariables, TOnMutateResult>useMutation, const useQueryClient: (queryClient?: QueryClient) => QueryClientuseQueryClient } from '@tanstack/react-query';
import type { type inferInfiniteQueryHandle<TProcedure extends DecorateInfiniteQueryProcedure<any>> = TProcedure extends DecorateInfiniteQueryProcedure<infer TDef extends ResolverDef> ? TRPCInfiniteQueryHandle<TDef> : neverinferInfiniteQueryHandle } from '@trpc/tanstack-react-query/unstable-handles';
import { const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
useTRPC, type type TRPC = DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>, {
...;
}> & DecorateRouterKeyable<...>
TRPC } from '~/trpc/client';
export function function AddComment(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
AddComment(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}
props: {
comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments: type inferInfiniteQueryHandle<TProcedure extends DecorateInfiniteQueryProcedure<any>> = TProcedure extends DecorateInfiniteQueryProcedure<infer TDef extends ResolverDef> ? TRPCInfiniteQueryHandle<TDef> : neverinferInfiniteQueryHandle<type TRPC = DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>, {
...;
}> & DecorateRouterKeyable<...>
TRPC['post']['comments']>;
}) {
const const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc = function useTRPC(): TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
useTRPC();
const const queryClient: QueryClientqueryClient = function useQueryClient(queryClient?: QueryClient): QueryClientuseQueryClient();
const const addComment: UseMutationResult<{
id: string;
text: string;
}, TRPCClientErrorLike<{
input: {
postId: string;
text: string;
};
output: {
id: string;
text: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, {
postId: string;
text: string;
}, undefined>
addComment = useMutation<{
id: string;
text: string;
}, TRPCClientErrorLike<{
input: {
postId: string;
text: string;
};
output: {
id: string;
text: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, {
postId: string;
text: string;
}, undefined>(options: UseMutationOptions<{
id: string;
text: string;
}, TRPCClientErrorLike<{
input: {
postId: string;
text: string;
};
output: {
id: string;
text: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, {
postId: string;
text: string;
}, undefined>, queryClient?: QueryClient): UseMutationResult<...>
useMutation(
const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.addComment: DecorateMutationProcedure<{
input: {
postId: string;
text: string;
};
output: {
id: string;
text: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
addComment.DecorateMutationProcedure<{ input: { postId: string; text: string; }; output: { id: string; text: string; }; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.mutationOptions: TRPCMutationOptions
<undefined>(opts?: TRPCMutationOptionsIn<{
postId: string;
text: string;
}, TRPCClientErrorLike<{
input: {
postId: string;
text: string;
};
output: {
id: string;
text: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, {
id: string;
text: string;
}, undefined, {
keyPrefix: false;
}> | undefined) => TRPCMutationOptionsOut<{
postId: string;
text: string;
}, TRPCClientErrorLike<{
input: {
postId: string;
text: string;
};
output: {
id: string;
text: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, {
id: string;
text: string;
}, undefined, {
keyPrefix: false;
}>
mutationOptions({
onSuccess?: ((data: {
id: string;
text: string;
}, variables: {
postId: string;
text: string;
}, onMutateResult: undefined, context: MutationFunctionContext) => Promise<unknown> | unknown) | undefined
onSuccess: () =>
const queryClient: QueryClientqueryClient.QueryClient.invalidateQueries<[path: string[], opts?: {
input?: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction"> | undefined;
type?: "infinite" | undefined;
} | undefined]>(filters?: InvalidateQueryFilters<[path: string[], opts?: {
input?: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction"> | undefined;
type?: "infinite" | undefined;
} | undefined]> | undefined, options?: InvalidateOptions): Promise<void>
invalidateQueries({
QueryFilters<[path: string[], opts?: { input?: Omit<{ postId: string; limit?: number | undefined; cursor?: number | null | undefined; }, "cursor" | "direction"> | undefined; type?: "infinite" | undefined; } | undefined]>.queryKey?: [path: string[], opts?: {
input?: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction"> | undefined;
type?: "infinite" | undefined;
} | undefined] | undefined
queryKey: props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}
props.comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments.TRPCInfiniteQueryHandle<{ input: { postId: string; limit?: number | undefined; cursor?: number | null | undefined; }; output: { items: { id: string; text: string; }[]; nextCursor: number | null; }; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.queryKey: [path: string[], opts?: {
input?: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction"> | undefined;
type?: "infinite" | undefined;
} | undefined]
queryKey,
QueryFilters<TQueryKey extends QueryKey = readonly unknown[]>.exact?: boolean | undefinedexact: true,
}),
}),
);
return (
<React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button
React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() =>
const addComment: UseMutationResult<{
id: string;
text: string;
}, TRPCClientErrorLike<{
input: {
postId: string;
text: string;
};
output: {
id: string;
text: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, {
postId: string;
text: string;
}, undefined>
addComment.mutate: (variables: {
postId: string;
text: string;
}, options?: MutateOptions<{
id: string;
text: string;
}, TRPCClientErrorLike<{
input: {
postId: string;
text: string;
};
output: {
id: string;
text: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, {
postId: string;
text: string;
}, undefined> | undefined) => void
mutate({ postId: stringpostId: props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}
props.comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments.TRPCInfiniteQueryHandle<{ input: { postId: string; limit?: number | undefined; cursor?: number | null | undefined; }; output: { items: { id: string; text: string; }[]; nextCursor: number | null; }; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.input: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction">
input.postId: stringpostId, text: stringtext: 'Nice!' })
}
>
Add a comment
</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>
);
}{ queryKey: props.comments.queryKey, exact: true } refreshes exactly the query the handle names. trpc.post.comments.queryFilter(…) would miss it, since the comments are an infinite query: see Invalidation.
Handles fix a gap in the usual prefetch pattern: the server prefetches trpc.post.byId.queryOptions({ id }), and the client repeats the same procedure and input:
- If the inputs differ, the read misses the prefetched data and silently fetches again.
- The client can read a different procedure from the one the server prefetched.
- Nothing shows that a prefetch happened at all.
With handles, the client reads exactly what the server started. They work best when a page can start every query from its URL: see Start every query from the URL.
A handle carries the query's key and input, but no data. The data lives in the cache: see Handles carry no data. The cache fills through TanStack Query's dehydrate and hydrate, as in the Server Components guide or TanStack Start's SSR query integration. In a server loader, return dehydrate(queryClient) with the handles, as in React Router.
The components that read handles are the same everywhere: with or without server components, and whatever loaded the handle.
Loading handles
A loader lists its queries as entries, and loadHandles(trpc, entries) starts them all at once, then returns a handle for each:
import {
function ensureEntry<TDef extends ResolverDef>(procedure: DecorateQueryProcedure<TDef>, ...args: EntryArgs<TDef>): TRPCQueryEntry<TDef, "ensure">ensureEntry,
function loadHandles<TFeatureFlags extends FeatureFlags, TEntries extends Record<string, AnyTRPCEntry>>(trpc: DecorateRouterKeyable<TFeatureFlags>, entries: TEntries): Promise<TRPCLoadedHandles<TEntries>>loadHandles,
function prefetchInfiniteEntry<TDef extends ResolverDef>(procedure: DecorateInfiniteQueryProcedure<TDef>, input: TDef["input"], opts?: TRPCInfiniteEntryOptions<TDef>): TRPCInfiniteQueryEntry<TDef, "prefetch">prefetchInfiniteEntry,
} from '@trpc/tanstack-react-query/unstable-handles';
const [const handles: HandlesOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
handles, const data: DataOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
data] = await loadHandles<FeatureFlags, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>(trpc: DecorateRouterKeyable<...>, entries: {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}): Promise<...>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
// Waits for the post, unless the cache has it
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
post: ensureEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
ensureEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid }),
// Doesn't wait
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
comments: prefetchInfiniteEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
...;
}, opts?: TRPCInfiniteEntryOptions<...> | undefined): TRPCInfiniteQueryEntry<...>
prefetchInfiniteEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments, { postId: stringpostId: const id: stringid }),
});
const handles: HandlesOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
handles.post;const data: DataOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
data.post;- Building an entry starts nothing. The load starts every entry before it waits for any, so the order of the entries doesn't matter, and you can't make one wait for another by accident.
- It resolves to
[handles, data]: a handle for every entry, by its key, and the data of the entries it waited for. Returnhandlesfrom the loader, or pass them to components as props. Reading a prefetch entry'sdatais a type error. - It rejects as soon as an entry it waits for fails, so you can handle the error where you load, for example to show a not-found page.
Each entry chooses whether the page waits for its query:
- Don't wait with
prefetchEntry. The page renders at once, and the data streams in later. - Wait with
ensureEntry. The page renders once the data is ready.
fetchEntry is a stricter form of waiting, for code that needs fresh data. See Waiting for fresh data.
Without waiting: prefetchEntry
const [const handles: HandlesOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
handles] = await loadHandles<FeatureFlags, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>(trpc: DecorateRouterKeyable<FeatureFlags>, entries: {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}): Promise<TRPCLoadedHandles<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
post: prefetchEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
prefetchEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid }),
});-
The load doesn't wait for it. The request runs in the background.
-
If the cache has no data for the query yet, the component that reads the handle renders before the data arrives, so it shows a loading state. If the cache has data, the component shows it while the request runs. With
useSuspenseQueryand no cached data, it suspends: wrap it in<Suspense>to show a fallback in its place. WithuseQuery, it renders withstatus: 'pending', and with server rendering it can cause a hydration mismatch: see Reading withuseQuery.<const Suspense: React.ExoticComponent<React.SuspenseProps>SuspenseReact.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading post…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}> <Postfunction Post(props: { post: inferQueryHandle<TRPC["post"]["byId"]>; }): JSX.Elementpost={post: TRPCQueryHandle<{ input: { id: string; }; output: Post; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>handles.const handles: HandlesOf<{ post: TRPCQueryEntry<{ input: { id: string; }; output: Post; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }, "prefetch">; }>post} /> </post: TRPCQueryHandle<{ input: { id: string; }; output: Post; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense>Without a boundary of your own, React shows the fallback of the nearest boundary above it, which may cover the whole page.
-
It never makes the load reject. If the request fails, the component that reads the handle gets the error:
useQueryreturns it asstatus: 'error', anduseSuspenseQuerythrows it to the nearest error boundary unless it has cached data to show. -
Its data isn't in
data: the query may not have any yet. Readingdata.postis a type error.
Use it for data the page can show a placeholder for: slow queries, content below the fold, secondary panels.
Waiting: ensureEntry
const [const handles: HandlesOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
handles, const data: DataOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
data] = await loadHandles<FeatureFlags, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>(trpc: DecorateRouterKeyable<FeatureFlags>, entries: {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}): Promise<TRPCLoadedHandles<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
post: ensureEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
ensureEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid }),
});- The load waits until the cache has data for the query, so the data is ready before anything renders.
data.posthas the data. See Using the data in a loader.- The component that reads the handle finds the data in the cache and renders it at once, with
useSuspenseQueryoruseQuery. It still needs a loading state for when the cache loses the data: see When the cache loses the data. - The load rejects if the request fails.
- If the cache already has stale data, the load resolves at once with it and refetches it in the background.
Use it for data the page can't render without, or data you need before rendering: the page title, a not-found check, a redirect.
Waiting for fresh data: fetchEntry
const [const handles: HandlesOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "fetch">;
}>
handles, const data: DataOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "fetch">;
}>
data] = await loadHandles<FeatureFlags, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "fetch">;
}>(trpc: DecorateRouterKeyable<FeatureFlags>, entries: {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "fetch">;
}): Promise<TRPCLoadedHandles<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "fetch">;
}>>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "fetch">
post: fetchEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "fetch">
fetchEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid }),
});- It works like
ensureEntry, with one difference: if the cache has stale data, it refetches and waits for the new data, likequeryClient.fetchQuery. - Use it when the loader reads the data and needs it fresh, for example a loader that redirects based on the post.
- In a browser loader, prefer
ensureEntry. With stale data, an ensure entry renders the page at once and updates it when the refetch lands. A fetch entry holds up the navigation until the refetch is done. - TanStack Router hides this difference on a page it has visited: by default, it shows the page with its last loader data and reruns the loader in the background. To wait there too, as the demo below does, set the router's
defaultStaleReloadMode, or a loader'sstaleReloadMode, to'blocking'. - In server components, the two behave the same: the request's
QueryClientstarts empty, so both wait for the request.
Which to use
prefetchEntry | ensureEntry | fetchEntry | |
|---|---|---|---|
| Page waits for data | No | Yes | Yes |
In data | No | Yes | Yes |
| With stale data | Refetches in the background | Resolves at once, refetches in the background | Waits for the refetch |
| Reader's first render | Loading state, or cached data | The data | The data |
| When the request fails | The reader gets the error | The load rejects | The load rejects |
The page shows at once, with skeletons until the data comes in. Stale data shows at once and refetches.
Comments
No requests yet.
TanStack Query equivalents
Each entry is one TanStack Query call with the procedure's queryOptions or infiniteQueryOptions. The load makes every call, then waits for the ones that aren't prefetches:
| tRPC | TanStack Query |
|---|---|
prefetchEntry(trpc.x, input, opts) | void queryClient.prefetchQuery(trpc.x.queryOptions(input, opts)) |
ensureEntry(trpc.x, input, opts) | await queryClient.ensureQueryData({ ...trpc.x.queryOptions(input, opts), revalidateIfStale: true }) |
fetchEntry(trpc.x, input, opts) | await queryClient.fetchQuery(trpc.x.queryOptions(input, opts)) |
prefetchInfiniteEntry, ensureInfiniteEntry, fetchInfiniteEntry | prefetchInfiniteQuery, ensureInfiniteQueryData (with revalidateIfStale: true), fetchInfiniteQuery |
fromHandle(trpc, handle, opts) | trpc.x.queryOptions(handle.input, opts), with x taken from the handle |
The queryClient is the one the proxy was created with. The entry names follow the TanStack Query method each one calls.
Waiting for some, streaming others
A page can wait for some queries and stream others. List them all in one load:
const [const handles: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
handles] = await loadHandles<FeatureFlags, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>(trpc: DecorateRouterKeyable<...>, entries: {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}): Promise<...>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
comments: prefetchInfiniteEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
...;
}, opts?: TRPCInfiniteEntryOptions<...> | undefined): TRPCInfiniteQueryEntry<...>
prefetchInfiniteEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments, { postId: stringpostId: const id: stringid }),
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
post: ensureEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
ensureEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid }),
});Here the post is ready when the page renders, and the comments stream in behind their own loading state. Both requests start together, whatever order you list them in.
Avoid awaiting one load and then starting another: the second load's requests wait for every entry the first one waited for. Use a second load only when its input comes from the first one's data: see Dependent queries.
Options
inputis optional when the procedure's input is optional, soprefetchEntry(trpc.viewer)works.optstakesstaleTime,gcTime,retry,retryDelay,meta, andtrpcrequest options such ascontext. They apply to the load's request. Readers use the options you pass tofromHandle.
Where to load handles
| Your app | Load handles in | With |
|---|---|---|
| Next.js App Router | Server components | The server proxy from trpc/server.tsx |
| TanStack Start, TanStack Router | Route loaders | The proxy in your router context |
| React Router | loader or clientLoader | See React Router |
| No loaders or server components, such as a SPA | Event handlers, such as a link's hover | useTRPC() |
- In server components, the request's
QueryClientstarts empty, so an ensure entry always waits for the request. loadHandlesand the entries take any tRPC options proxy: one fromcreateTRPCOptionsProxy, oruseTRPC(). Type a router context withTRPCOptionsProxy<AppRouter>.- Load handles where the code can wait: a loader, a server component, or an event handler, such as preloading a page on hover. Don't call
loadHandlesduring render: every render starts its entries again, and a render can't wait for them. A component that renders before anything loaded its query reads it directly, withuseSuspenseQuery(trpc.x.queryOptions(input)). loadHandlestakes the root proxy,trpc, and one load can take entries from any procedure.
Start every query from the URL
A loader or server component runs before anything renders, and all it has is the URL: the route's params, its search params, and on the server, the session. If every query a page needs takes its input from those, every request starts the moment the navigation does. No component has to render first to find out what to fetch.
- Path params name what the page is about:
/posts/$slug. - Search params hold everything else that changes what a query fetches: filters, sort, page. Keep them out of component state. A loader can't see component state, so the query would start only after render, the back button wouldn't restore the view, and a shared link would open the default one.
- Typesafe APIs without codegen▲ 3
- Streaming responses over HTTP▲ 40
- Subscriptions with server-sent events▲ 27
What the loader sees
loaderDeps: ({ search }) => search,
// { sort: 'new', page: 1 }
loader: ({ deps }) =>
loadHandles(trpc, {
posts: prefetchEntry(trpc.post.list, deps),
}),Requests
- loaderpost.list({ sort: 'new', page: 1 })done
If the URL isn't enough, reshape the procedure
If a page can't start a query from the URL alone, change the procedure, not the page. A procedure that takes an id you only learn from another query forces a waterfall on every page that calls it. Two fixes:
- Take what the URL has. Accept the slug or route param, and look up the id on the server.
- Return the joined data. Return the post with its author, not an author id for the client to look up.
The server sits next to the database, so the extra lookup costs a query there, not another round trip from the browser.
Before: the page is /posts/$slug, but the comments and the author need ids that only the post has:
import { import zz } from 'zod';
import { const db: {
post: {
bySlug(slug: string): Promise<Post>;
};
author: {
byId(id: string): Promise<Author>;
};
comment: {
list(opts: {
postId: string;
cursor: number | null;
}): Promise<{
items: Comment[];
nextCursor: number | null;
}>;
};
}
db } from './db';
import { const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t } from './trpc';
export const const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
author: BuiltRouter<...>;
}>>
appRouter = const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder
<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
author: BuiltRouter<...>;
}>(_: {
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
author: BuiltRouter<...>;
}) => BuiltRouter<...>
router({
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
post: const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder
<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>(_: {
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}) => BuiltRouter<...>
router({
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>
bySlug: const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure
.ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.input<z.ZodObject<{
slug: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
slug: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
slug: string;
}, {
slug: string;
}, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
input(import zz.function object<{
slug: z.ZodString;
}>(shape?: {
slug: z.ZodString;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
slug: z.ZodString;
}, z.core.$strip>
object({ slug: z.ZodStringslug: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() }))
.ProcedureBuilder<object, object, object, { slug: string; }, { slug: string; }, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.query<Post>(resolver: ProcedureResolver<object, object, object, {
slug: string;
}, UnsetMarker, Post>): QueryProcedure<{
input: {
slug: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>
query(({ input: {
slug: string;
}
input }) => const db: {
post: {
bySlug(slug: string): Promise<Post>;
};
author: {
byId(id: string): Promise<Author>;
};
comment: {
list(opts: {
postId: string;
cursor: number | null;
}): Promise<{
items: Comment[];
nextCursor: number | null;
}>;
};
}
db.post: {
bySlug(slug: string): Promise<Post>;
}
post.function bySlug(slug: string): Promise<Post>bySlug(input: {
slug: string;
}
input.slug: stringslug)),
comments: QueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>
comments: const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure
.ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.input<z.ZodObject<{
postId: z.ZodString;
cursor: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
}, z.core.$strip>>(schema: z.ZodObject<{
postId: z.ZodString;
cursor: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
postId: string;
cursor?: number | null | undefined;
}, {
postId: string;
cursor?: number | null | undefined;
}, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
input(
import zz.function object<{
postId: z.ZodString;
cursor: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
}>(shape?: {
postId: z.ZodString;
cursor: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
postId: z.ZodString;
cursor: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
}, z.core.$strip>
object({ postId: z.ZodStringpostId: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string(), cursor: z.ZodOptional<z.ZodNullable<z.ZodNumber>>cursor: import zz.function number(params?: string | z.core.$ZodNumberParams): z.ZodNumbernumber().ZodType<any, any, $ZodNumberInternals<number>>.nullish(): z.ZodOptional<z.ZodNullable<z.ZodNumber>>nullish() }),
)
.ProcedureBuilder<object, object, object, { postId: string; cursor?: number | null | undefined; }, { postId: string; cursor?: number | null | undefined; }, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.query<{
items: Comment[];
nextCursor: number | null;
}>(resolver: ProcedureResolver<object, object, object, {
postId: string;
cursor?: number | null | undefined;
}, UnsetMarker, {
items: Comment[];
nextCursor: number | null;
}>): QueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>
query(({ input: {
postId: string;
cursor?: number | null | undefined;
}
input }) =>
const db: {
post: {
bySlug(slug: string): Promise<Post>;
};
author: {
byId(id: string): Promise<Author>;
};
comment: {
list(opts: {
postId: string;
cursor: number | null;
}): Promise<{
items: Comment[];
nextCursor: number | null;
}>;
};
}
db.comment: {
list(opts: {
postId: string;
cursor: number | null;
}): Promise<{
items: Comment[];
nextCursor: number | null;
}>;
}
comment.function list(opts: {
postId: string;
cursor: number | null;
}): Promise<{
items: Comment[];
nextCursor: number | null;
}>
list({
postId: stringpostId: input: {
postId: string;
cursor?: number | null | undefined;
}
input.postId: stringpostId,
cursor: number | nullcursor: input: {
postId: string;
cursor?: number | null | undefined;
}
input.cursor?: number | null | undefinedcursor ?? null,
}),
),
}),
author: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Author;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
author: const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder
<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Author;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>(_: {
byId: QueryProcedure<{
input: {
id: string;
};
output: Author;
meta: object;
errorShape: DefaultErrorShape;
}>;
}) => BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Author;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
router({
byId: QueryProcedure<{
input: {
id: string;
};
output: Author;
meta: object;
errorShape: DefaultErrorShape;
}>
byId: const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure
.ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.input<z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
id: string;
}, {
id: string;
}, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
input(import zz.function object<{
id: z.ZodString;
}>(shape?: {
id: z.ZodString;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>
object({ id: z.ZodStringid: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() }))
.ProcedureBuilder<object, object, object, { id: string; }, { id: string; }, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.query<Author>(resolver: ProcedureResolver<object, object, object, {
id: string;
}, UnsetMarker, Author>): QueryProcedure<{
input: {
id: string;
};
output: Author;
meta: object;
errorShape: DefaultErrorShape;
}>
query(({ input: {
id: string;
}
input }) => const db: {
post: {
bySlug(slug: string): Promise<Post>;
};
author: {
byId(id: string): Promise<Author>;
};
comment: {
list(opts: {
postId: string;
cursor: number | null;
}): Promise<{
items: Comment[];
nextCursor: number | null;
}>;
};
}
db.author: {
byId(id: string): Promise<Author>;
}
author.function byId(id: string): Promise<Author>byId(input: {
id: string;
}
input.id: stringid)),
}),
});async function function loader(params: {
slug: string;
}): Promise<{
comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>;
author: TRPCQueryHandle<{
input: {
id: string;
};
output: Author;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>;
post: TRPCQueryHandle<{
input: {
slug: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>;
}>
loader(params: {
slug: string;
}
params: { slug: stringslug: string }) {
const [const first: HandlesOf<{
post: TRPCQueryEntry<{
input: {
slug: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
first, { const post: Postpost }] = await loadHandles<FeatureFlags, {
post: TRPCQueryEntry<{
input: {
slug: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>(trpc: DecorateRouterKeyable<FeatureFlags>, entries: {
post: TRPCQueryEntry<{
input: {
slug: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}): Promise<TRPCLoadedHandles<{
post: TRPCQueryEntry<{
input: {
slug: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
author: BuiltRouter<...>;
}>>, {
...;
}>
trpc, {
post: TRPCQueryEntry<{
input: {
slug: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
post: ensureEntry<{
input: {
slug: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
slug: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
slug: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
slug: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
ensureEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
author: BuiltRouter<...>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.bySlug: DecorateQueryProcedure<{
input: {
slug: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
bySlug, { slug: stringslug: params: {
slug: string;
}
params.slug: stringslug }),
});
// Nothing below can start until the post has arrived
const [const second: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
author: TRPCQueryEntry<{
input: {
id: string;
};
output: Author;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
second] = await loadHandles<FeatureFlags, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
author: TRPCQueryEntry<{
input: {
id: string;
};
output: Author;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>(trpc: DecorateRouterKeyable<...>, entries: {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
author: TRPCQueryEntry<{
input: {
id: string;
};
output: Author;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}): Promise<...>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
author: BuiltRouter<...>;
}>>, {
...;
}>
trpc, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
comments: prefetchInfiniteEntry<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateInfiniteQueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
...;
}, opts?: TRPCInfiniteEntryOptions<...> | undefined): TRPCInfiniteQueryEntry<...>
prefetchInfiniteEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
author: BuiltRouter<...>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments, {
postId: stringpostId: const post: Postpost.id: stringid,
}),
author: TRPCQueryEntry<{
input: {
id: string;
};
output: Author;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
author: prefetchEntry<{
input: {
id: string;
};
output: Author;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Author;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: Author;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
prefetchEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
author: BuiltRouter<...>;
}>>, {
...;
}>
trpc.author: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Author;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>, {
...;
}> & DecorateRouterKeyable<...>
author.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Author;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid: const post: Postpost.authorId: stringauthorId }),
});
return { ...const first: HandlesOf<{
post: TRPCQueryEntry<{
input: {
slug: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
first, ...const second: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
author: TRPCQueryEntry<{
input: {
id: string;
};
output: Author;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
second };
}post.bySlug |-------|
post.comments |-------|
author.byId |-------|
After: post.comments takes the slug, and post.bySlug returns the post with its author:
import { import zz } from 'zod';
import { const db: {
post: {
bySlug(slug: string): Promise<Post>;
};
author: {
byId(id: string): Promise<Author>;
};
comment: {
list(opts: {
postId: string;
cursor: number | null;
}): Promise<{
items: Comment[];
nextCursor: number | null;
}>;
};
}
db } from './db';
import { const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t } from './trpc';
export const const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
appRouter = const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder
<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>(_: {
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}) => BuiltRouter<...>
router({
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
post: const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder
<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>(_: {
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}) => BuiltRouter<...>
router({
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>
bySlug: const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure
.ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.input<z.ZodObject<{
slug: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
slug: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
slug: string;
}, {
slug: string;
}, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
input(import zz.function object<{
slug: z.ZodString;
}>(shape?: {
slug: z.ZodString;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
slug: z.ZodString;
}, z.core.$strip>
object({ slug: z.ZodStringslug: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() }))
.ProcedureBuilder<object, object, object, { slug: string; }, { slug: string; }, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.query<{
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
}>(resolver: ProcedureResolver<object, object, object, {
slug: string;
}, UnsetMarker, {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
}>): QueryProcedure<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>
query(async ({ input: {
slug: string;
}
input }) => {
const const post: Postpost = await const db: {
post: {
bySlug(slug: string): Promise<Post>;
};
author: {
byId(id: string): Promise<Author>;
};
comment: {
list(opts: {
postId: string;
cursor: number | null;
}): Promise<{
items: Comment[];
nextCursor: number | null;
}>;
};
}
db.post: {
bySlug(slug: string): Promise<Post>;
}
post.function bySlug(slug: string): Promise<Post>bySlug(input: {
slug: string;
}
input.slug: stringslug);
return { ...const post: Postpost, author: Authorauthor: await const db: {
post: {
bySlug(slug: string): Promise<Post>;
};
author: {
byId(id: string): Promise<Author>;
};
comment: {
list(opts: {
postId: string;
cursor: number | null;
}): Promise<{
items: Comment[];
nextCursor: number | null;
}>;
};
}
db.author: {
byId(id: string): Promise<Author>;
}
author.function byId(id: string): Promise<Author>byId(const post: Postpost.authorId: stringauthorId) };
}),
comments: QueryProcedure<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>
comments: const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure
.ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.input<z.ZodObject<{
slug: z.ZodString;
cursor: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
}, z.core.$strip>>(schema: z.ZodObject<{
slug: z.ZodString;
cursor: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
slug: string;
cursor?: number | null | undefined;
}, {
slug: string;
cursor?: number | null | undefined;
}, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
input(
import zz.function object<{
slug: z.ZodString;
cursor: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
}>(shape?: {
slug: z.ZodString;
cursor: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
slug: z.ZodString;
cursor: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
}, z.core.$strip>
object({ slug: z.ZodStringslug: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string(), cursor: z.ZodOptional<z.ZodNullable<z.ZodNumber>>cursor: import zz.function number(params?: string | z.core.$ZodNumberParams): z.ZodNumbernumber().ZodType<any, any, $ZodNumberInternals<number>>.nullish(): z.ZodOptional<z.ZodNullable<z.ZodNumber>>nullish() }),
)
.ProcedureBuilder<object, object, object, { slug: string; cursor?: number | null | undefined; }, { slug: string; cursor?: number | null | undefined; }, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.query<{
items: Comment[];
nextCursor: number | null;
}>(resolver: ProcedureResolver<object, object, object, {
slug: string;
cursor?: number | null | undefined;
}, UnsetMarker, {
items: Comment[];
nextCursor: number | null;
}>): QueryProcedure<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>
query(async ({ input: {
slug: string;
cursor?: number | null | undefined;
}
input }) => {
const const post: Postpost = await const db: {
post: {
bySlug(slug: string): Promise<Post>;
};
author: {
byId(id: string): Promise<Author>;
};
comment: {
list(opts: {
postId: string;
cursor: number | null;
}): Promise<{
items: Comment[];
nextCursor: number | null;
}>;
};
}
db.post: {
bySlug(slug: string): Promise<Post>;
}
post.function bySlug(slug: string): Promise<Post>bySlug(input: {
slug: string;
cursor?: number | null | undefined;
}
input.slug: stringslug);
return const db: {
post: {
bySlug(slug: string): Promise<Post>;
};
author: {
byId(id: string): Promise<Author>;
};
comment: {
list(opts: {
postId: string;
cursor: number | null;
}): Promise<{
items: Comment[];
nextCursor: number | null;
}>;
};
}
db.comment: {
list(opts: {
postId: string;
cursor: number | null;
}): Promise<{
items: Comment[];
nextCursor: number | null;
}>;
}
comment.function list(opts: {
postId: string;
cursor: number | null;
}): Promise<{
items: Comment[];
nextCursor: number | null;
}>
list({
postId: stringpostId: const post: Postpost.id: stringid,
cursor: number | nullcursor: input: {
slug: string;
cursor?: number | null | undefined;
}
input.cursor?: number | null | undefinedcursor ?? null,
});
}),
}),
});async function function loader(params: {
slug: string;
}): Promise<HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>>
loader(params: {
slug: string;
}
params: { slug: stringslug: string }) {
// Both start from the URL, together
const [const handles: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
handles] = await loadHandles<FeatureFlags, {
comments: TRPCInfiniteQueryEntry<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>(trpc: DecorateRouterKeyable<...>, entries: {
comments: TRPCInfiniteQueryEntry<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}): Promise<...>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
trpc, {
comments: TRPCInfiniteQueryEntry<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
comments: prefetchInfiniteEntry<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateInfiniteQueryProcedure<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
...;
}, opts?: TRPCInfiniteEntryOptions<...> | undefined): TRPCInfiniteQueryEntry<...>
prefetchInfiniteEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments, {
slug: stringslug: params: {
slug: string;
}
params.slug: stringslug,
}),
post: TRPCQueryEntry<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
post: ensureEntry<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
slug: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<...>
ensureEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
bySlug: QueryProcedure<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.bySlug: DecorateQueryProcedure<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
bySlug, { slug: stringslug: params: {
slug: string;
}
params.slug: stringslug }),
});
return const handles: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
slug: string;
cursor?: number | null | undefined;
};
output: {
items: Comment[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
slug: string;
};
output: {
author: Author;
id: string;
slug: string;
title: string;
authorId: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
handles;
}post.bySlug |-------|
post.comments |-------|
The page is ready after one round trip instead of two, and the loader no longer reads the post to start the other queries.
Reading a handle
fromHandle(trpc, handle, opts?) returns the same options as trpc.<path>.queryOptions(input, opts) for the handle's procedure and input, or trpc.<path>.infiniteQueryOptions(input, opts) for an infinite handle. Pass them to any TanStack Query hook or QueryClient method:
const const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc = function useTRPC(): TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
useTRPC();
const const postQuery: UseSuspenseQueryResult<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>>
postQuery = useSuspenseQuery<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, Post, TRPCQueryKeyWithoutPrefix>(options: UseSuspenseQueryOptions<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, Post, TRPCQueryKeyWithoutPrefix>, queryClient?: QueryClient): UseSuspenseQueryResult<...>
useSuspenseQuery(fromHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, Post>(trpc: DecorateRouterKeyable<NoInfer<{
keyPrefix: false;
}>>, handle: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, opts?: UnusedSkipTokenTRPCQueryOptionsIn<NoInfer<Post>, Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, {
keyPrefix: false;
}> | undefined): UnusedSkipTokenTRPCQueryOptionsOut<...> (+1 overload)
fromHandle(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, const props: {
post: PostHandle;
a: PostHandle;
b: PostHandle;
}
props.post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post));
const const titleQuery: UseQueryResult<string, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>>
titleQuery = useQuery<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, string, TRPCQueryKeyWithoutPrefix>(options: UndefinedInitialDataOptions<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, string, TRPCQueryKeyWithoutPrefix>, queryClient?: QueryClient): UseQueryResult<...> (+2 overloads)
useQuery(fromHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, string>(trpc: DecorateRouterKeyable<NoInfer<{
keyPrefix: false;
}>>, handle: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, opts?: UnusedSkipTokenTRPCQueryOptionsIn<NoInfer<Post>, string, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, {
keyPrefix: false;
}> | undefined): UnusedSkipTokenTRPCQueryOptionsOut<...> (+1 overload)
fromHandle(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, const props: {
post: PostHandle;
a: PostHandle;
b: PostHandle;
}
props.post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post, { select?: ((data: NoInfer<Post>) => string) | undefinedselect: (post: NoInfer<Post>post) => post: Postpost.title: stringtitle }));
const [const aQuery: UseSuspenseQueryResult<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>>
aQuery, const bQuery: UseSuspenseQueryResult<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>>
bQuery] = useSuspenseQueries<[UnusedSkipTokenTRPCQueryOptionsOut<Post, Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, {
keyPrefix: false;
}>, UnusedSkipTokenTRPCQueryOptionsOut<Post, Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, {
keyPrefix: false;
}>], [...]>(options: {
...;
}, queryClient?: QueryClient): [...] (+1 overload)
useSuspenseQueries({
queries: readonly [UseSuspenseQueryOptions<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, Post, TRPCQueryKeyWithoutPrefix>, UseSuspenseQueryOptions<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, Post, TRPCQueryKeyWithoutPrefix>]
queries: [fromHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, Post>(trpc: DecorateRouterKeyable<NoInfer<{
keyPrefix: false;
}>>, handle: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, opts?: UnusedSkipTokenTRPCQueryOptionsIn<NoInfer<Post>, Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, {
keyPrefix: false;
}> | undefined): UnusedSkipTokenTRPCQueryOptionsOut<...> (+1 overload)
fromHandle(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, const props: {
post: PostHandle;
a: PostHandle;
b: PostHandle;
}
props.a: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
a), fromHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, Post>(trpc: DecorateRouterKeyable<NoInfer<{
keyPrefix: false;
}>>, handle: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, opts?: UnusedSkipTokenTRPCQueryOptionsIn<NoInfer<Post>, Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, {
keyPrefix: false;
}> | undefined): UnusedSkipTokenTRPCQueryOptionsOut<...> (+1 overload)
fromHandle(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, const props: {
post: PostHandle;
a: PostHandle;
b: PostHandle;
}
props.b: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
b)],
});optstakes everythingqueryOptionstakes, such asselect,staleTime,refetchIntervalandtrpc.fromHandletakes any level of the proxy:fromHandle(trpc, handle)andfromHandle(trpc.post, handle)behave the same.- Reads use the same query key as
trpc.<path>.queryOptions(input), so old and new code share cache entries. handle.queryKeyis the cache entry's key. For a typedqueryClient.getQueryData, passfromHandle(trpc, handle).queryKey. The handle's own key isn't tagged with its data type, so the handle stays serializable everywhere, including TanStack Router'sbeforeLoad.handle.inputis the typed input the query was started with, so a component doesn't need it as a separate prop. Infinite handles leave outcursoranddirection.
Typing props
Export your proxy type once, then type props with inferQueryHandle:
export type type TRPC = DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>, {
...;
}> & DecorateRouterKeyable<...>
TRPC = type TRPCOptionsProxy<TRouter extends AnyTRPCRouter, TFeatureFlags extends FeatureFlags = { keyPrefix: false; }> = DecoratedRouterRecord<TRouter["_def"]["_config"]["$types"], TRouter["_def"]["record"], TFeatureFlags> & DecorateRouterKeyable<TFeatureFlags>TRPCOptionsProxy<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter>;'use client';
import { function useSuspenseQuery<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = readonly unknown[]>(options: UseSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): UseSuspenseQueryResult<TData, TError>useSuspenseQuery } from '@tanstack/react-query';
import { function fromHandle<TDef extends ResolverDef, TData = TDef["output"]>(trpc: DecorateRouterKeyable<NoInfer<TDef["featureFlags"]>>, handle: TRPCQueryHandle<TDef>, opts?: UnusedSkipTokenTRPCQueryOptionsIn<NoInfer<TDef["output"]>, TData, ErrorOf<TDef>, TDef["featureFlags"]>): UnusedSkipTokenTRPCQueryOptionsOut<TDef["output"], TData, ErrorOf<TDef>, TDef["featureFlags"]> (+1 overload)fromHandle } from '@trpc/tanstack-react-query/unstable-handles';
import type { type inferQueryHandle<TProcedure extends DecorateQueryProcedure<any>> = TProcedure extends DecorateQueryProcedure<infer TDef extends ResolverDef> ? TRPCQueryHandle<TDef> : neverinferQueryHandle } from '@trpc/tanstack-react-query/unstable-handles';
import { const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
useTRPC, type type TRPC = DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>, {
...;
}> & DecorateRouterKeyable<...>
TRPC } from '~/trpc/client';
export function function Post(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}): JSX.Element
Post(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}
props: { post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post: type inferQueryHandle<TProcedure extends DecorateQueryProcedure<any>> = TProcedure extends DecorateQueryProcedure<infer TDef extends ResolverDef> ? TRPCQueryHandle<TDef> : neverinferQueryHandle<type TRPC = DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>, {
...;
}> & DecorateRouterKeyable<...>
TRPC['post']['byId']> }) {
const const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc = function useTRPC(): TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
useTRPC();
const const postQuery: UseSuspenseQueryResult<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>>
postQuery = useSuspenseQuery<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, Post, TRPCQueryKeyWithoutPrefix>(options: UseSuspenseQueryOptions<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, Post, TRPCQueryKeyWithoutPrefix>, queryClient?: QueryClient): UseSuspenseQueryResult<...>
useSuspenseQuery(fromHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, Post>(trpc: DecorateRouterKeyable<NoInfer<{
keyPrefix: false;
}>>, handle: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, opts?: UnusedSkipTokenTRPCQueryOptionsIn<NoInfer<Post>, Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, {
keyPrefix: false;
}> | undefined): UnusedSkipTokenTRPCQueryOptionsOut<...> (+1 overload)
fromHandle(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}
props.post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post));
return <React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const postQuery: UseSuspenseQueryResult<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>>
postQuery.data: Postdata.title: stringtitle}</React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>;
}A handle can only come from loadHandles. A raw input, an entry, or an object with the right queryKey and input written by hand, doesn't type-check. In tests, load one over a seeded cache: see Testing components that take handles. At runtime, fromHandle also throws a readable error for anything that isn't a handle, and when the handle's keyPrefix differs from the reading proxy's.
inferQueryHandle accepts a handle from any entry: they all have the same type.
Reading with useQuery
You don't need useSuspenseQuery or a Suspense boundary to read a handle. useQuery works with every handle, as for any query. Switch on its status to show each state:
'use client';
import { function useQuery<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = readonly unknown[]>(options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): DefinedUseQueryResult<TData, TError> (+2 overloads)useQuery } from '@tanstack/react-query';
import { function fromHandle<TDef extends ResolverDef, TData = TDef["output"]>(trpc: DecorateRouterKeyable<NoInfer<TDef["featureFlags"]>>, handle: TRPCQueryHandle<TDef>, opts?: UnusedSkipTokenTRPCQueryOptionsIn<NoInfer<TDef["output"]>, TData, ErrorOf<TDef>, TDef["featureFlags"]>): UnusedSkipTokenTRPCQueryOptionsOut<TDef["output"], TData, ErrorOf<TDef>, TDef["featureFlags"]> (+1 overload)fromHandle } from '@trpc/tanstack-react-query/unstable-handles';
import type { type inferQueryHandle<TProcedure extends DecorateQueryProcedure<any>> = TProcedure extends DecorateQueryProcedure<infer TDef extends ResolverDef> ? TRPCQueryHandle<TDef> : neverinferQueryHandle } from '@trpc/tanstack-react-query/unstable-handles';
import { const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
useTRPC, type type TRPC = DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>, {
...;
}> & DecorateRouterKeyable<...>
TRPC } from '~/trpc/client';
export function function Post(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}): JSX.Element
Post(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}
props: { post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post: type inferQueryHandle<TProcedure extends DecorateQueryProcedure<any>> = TProcedure extends DecorateQueryProcedure<infer TDef extends ResolverDef> ? TRPCQueryHandle<TDef> : neverinferQueryHandle<type TRPC = DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>, {
...;
}> & DecorateRouterKeyable<...>
TRPC['post']['byId']> }) {
const const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc = function useTRPC(): TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
useTRPC();
const const postQuery: UseQueryResult<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>>
postQuery = useQuery<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, Post, TRPCQueryKeyWithoutPrefix>(options: UndefinedInitialDataOptions<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, Post, TRPCQueryKeyWithoutPrefix>, queryClient?: QueryClient): UseQueryResult<...> (+2 overloads)
useQuery(fromHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, Post>(trpc: DecorateRouterKeyable<NoInfer<{
keyPrefix: false;
}>>, handle: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, opts?: UnusedSkipTokenTRPCQueryOptionsIn<NoInfer<Post>, Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, {
keyPrefix: false;
}> | undefined): UnusedSkipTokenTRPCQueryOptionsOut<...> (+1 overload)
fromHandle(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}
props.post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post));
switch (const postQuery: UseQueryResult<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>>
postQuery.status: "pending" | "error" | "success"status) {
case 'pending':
return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading post…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>;
case 'error':
return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Couldn't load the post: {const postQuery: QueryObserverRefetchErrorResult<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>> | QueryObserverLoadingErrorResult<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>>
postQuery.error: ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
error.TRPCClientErrorBase<TShape extends DefaultErrorShape>.message: stringmessage}</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>;
case 'success':
return <React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const postQuery: QueryObserverSuccessResult<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>> | QueryObserverPlaceholderResult<Post, ErrorOf<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>>
postQuery.data: Postdata.title: stringtitle}</React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>;
}
}useQuerydoesn't suspend. Until the data arrives, it renders withstatus: 'pending'.- After an ensure or fetch entry, the cache has the data, so the first render has
status: 'success'. - With server rendering, read a prefetch entry's handle with
useSuspenseQuery: withuseQuery, the server can render the loading state while the browser already has the data, and React reports a hydration mismatch (TanStack/query#9399). For a component onuseQuery, wait for the data instead, with an ensure entry.
Handles carry no data
A handle is only the query's key and input, whichever entry made it. The data lives in the cache, so the reader decides how to wait for it:
useSuspenseQuerytypesdataas defined. The component suspends until the cache has the data.useQuerytypesdataas possiblyundefined, as for any query. Switch onstatusto narrow it.
When the cache loses the data
Waiting for a query with an ensure or fetch entry puts its data in the cache before the reader renders. The cache can still lose that data later, and the reader then has to load it again:
queryClient.resetQueries()clears the data and refetches the queries on screen at once. AuseSuspenseQueryreader suspends and shows the nearestSuspensefallback. AuseQueryreader renders withstatus: 'pending'.queryClient.removeQueries()andqueryClient.clear(), for example on logout, drop the query from the cache. A reader on screen keeps its data until it renders again. Then it loads the query as above.- TanStack Query also drops a query no component has used for its
gcTime. A reader that mounts after that loads it again. queryClient.invalidateQueries()keeps the data and refetches in the background, so the reader shows no loading state.
So give every reader a loading state, even when the page waited for its data: put a useSuspenseQuery reader under a Suspense boundary, or handle 'pending' in a useQuery reader. Without a boundary of its own, a reader that suspends shows the fallback of the nearest boundary above it, which may cover the whole page.
The reader loads the query again with a new request. A handle doesn't bring back the data the loader started with.
This is how readers behave with the default options. Some options change it: with initialData, resetQueries() puts that data back, and with enabled: false, a reader that lost its data stays pending and starts no request.
Using the data in a loader
To use the data where you load it, for example to set the page title or redirect, read it from the load's data. It has the data of every ensure and fetch entry, by the entry's key:
const [const handles: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
handles, const data: DataOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
data] = await loadHandles<FeatureFlags, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>(trpc: DecorateRouterKeyable<...>, entries: {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}): Promise<...>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
comments: prefetchInfiniteEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
...;
}, opts?: TRPCInfiniteEntryOptions<...> | undefined): TRPCInfiniteQueryEntry<...>
prefetchInfiniteEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments, { postId: stringpostId: const id: stringid }),
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
post: ensureEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
ensureEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid }),
});
if (!const data: DataOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
data.post.title: stringtitle) function notFound(): nevernotFound();datahas no key for prefetch entries, since their query may have no data yet.- The data is the same object the cache holds. Return
handles, notdata, so it doesn't cross to the browser twice. - Don't build other queries' inputs from it. Start them from the URL instead, as in Start every query from the URL. Otherwise, see Dependent queries.
Infinite queries
The post page's comments and more by its author are lists, so they're infinite queries. For procedures with a cursor input, use prefetchInfiniteEntry, ensureInfiniteEntry and fetchInfiniteEntry. They load the first page. To load more pages up front, pass pages with getNextPageParam:
const [const handles: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
handles, const data: DataOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
data] = await loadHandles<FeatureFlags, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>(trpc: DecorateRouterKeyable<FeatureFlags>, entries: {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}): Promise<...>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
comments: ensureInfiniteEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
...;
}, opts?: TRPCInfiniteEntryOptions<...> | undefined): TRPCInfiniteQueryEntry<...>
ensureInfiniteEntry(
const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments,
{ postId: stringpostId: const id: stringid, limit?: number | undefinedlimit: 20 },
{ pages: numberpages: 3, getNextPageParam: HandleGetNextPageParam<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
getNextPageParam: (last: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}
last) => last: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}
last.nextCursor: number | nullnextCursor },
),
});
const data: DataOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
data.comments: TRPCInfiniteData<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>
comments.pages;pages applies when the cache has no pages for the query. If it already has some, an ensure entry resolves with those, and if a request for the query is already in flight, the entry joins it and gets the pages that request loads. Two entries for the same query share one cache entry, so list each query once.
Functions can't cross to the client, so pass getNextPageParam when you read an infinite handle:
const const commentsQuery: UseSuspenseInfiniteQueryResult<TRPCInfiniteData<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>, ErrorOf<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>>
commentsQuery = useSuspenseInfiniteQuery<{
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}, ErrorOf<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, TRPCInfiniteData<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>, TRPCQueryKeyWithoutPrefix, number | null>(options: UseSuspenseInfiniteQueryOptions<...>, queryClient?: QueryClient): UseSuspenseInfiniteQueryResult<...>
useSuspenseInfiniteQuery(
fromHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>(trpc: DecorateRouterKeyable<NoInfer<{
keyPrefix: false;
}>>, handle: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, opts: UnusedSkipTokenTRPCInfiniteQueryOptionsIn<...>): UnusedSkipTokenTRPCInfiniteQueryOptionsOut<...> (+1 overload)
fromHandle(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, const props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}
props.comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments, {
getNextPageParam: GetNextPageParamFunction<number | null, NoInfer<{
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>>
getNextPageParam: (last: NoInfer<{
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>
last) => last: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}
last.nextCursor: number | nullnextCursor,
}),
);Type the prop with inferInfiniteQueryHandle<TRPC['post']['comments']>.
In the browser, a loader can run again while the cache holds every page the user loaded. If that data is stale, the entry refetches all of those pages, which needs getNextPageParam from page two on. Pass the reader's getNextPageParam to entries in loaders that run in the browser:
const [const handles: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
handles] = await loadHandles<FeatureFlags, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>(trpc: DecorateRouterKeyable<FeatureFlags>, entries: {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}): Promise<...>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
comments: prefetchInfiniteEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
...;
}, opts?: TRPCInfiniteEntryOptions<...> | undefined): TRPCInfiniteQueryEntry<...>
prefetchInfiniteEntry(
const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments,
{ postId: stringpostId: const id: stringid, limit?: number | undefinedlimit: 20 },
{ getNextPageParam: (last: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}) => number | null
getNextPageParam: (last: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}
last) => last: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}
last.nextCursor: number | nullnextCursor },
),
});Changing a handle's input
Say the post page's comments can be sorted. A handle names one input, so to read another, call the procedure directly, starting from the handle's input:
import { function useSuspenseInfiniteQuery<TQueryFnData, TError = Error, TData = InfiniteData<TQueryFnData, unknown>, TQueryKey extends QueryKey = readonly unknown[], TPageParam = unknown>(options: UseSuspenseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>, queryClient?: QueryClient): UseSuspenseInfiniteQueryResult<TData, TError>useSuspenseInfiniteQuery } from '@tanstack/react-query';
import type { type inferInfiniteQueryHandle<TProcedure extends DecorateInfiniteQueryProcedure<any>> = TProcedure extends DecorateInfiniteQueryProcedure<infer TDef extends ResolverDef> ? TRPCInfiniteQueryHandle<TDef> : neverinferInfiniteQueryHandle } from '@trpc/tanstack-react-query/unstable-handles';
import { function useState<S>(initialState: S | (() => S)): [S, React.Dispatch<React.SetStateAction<S>>] (+1 overload)useState } from 'react';
export function function Comments(props: {
comments: inferInfiniteQueryHandle<typeof serverTRPC.post.comments>;
}): JSX.Element
Comments(props: {
comments: inferInfiniteQueryHandle<typeof serverTRPC.post.comments>;
}
props: {
comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments: type inferInfiniteQueryHandle<TProcedure extends DecorateInfiniteQueryProcedure<any>> = TProcedure extends DecorateInfiniteQueryProcedure<infer TDef extends ResolverDef> ? TRPCInfiniteQueryHandle<TDef> : neverinferInfiniteQueryHandle<typeof const serverTRPC: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
comments: QueryProcedure<{
input: {
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
serverTRPC.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
comments: QueryProcedure<{
input: {
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments>;
}) {
const const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
comments: QueryProcedure<{
input: {
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
trpc = const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
comments: QueryProcedure<{
input: {
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
useTRPC();
// Start from the loader's input, so the first render reads the query it started
const [const sort: "new" | "top"sort, const setSort: React.Dispatch<React.SetStateAction<"new" | "top">>setSort] = useState<"new" | "top">(initialState: "new" | "top" | (() => "new" | "top")): ["new" | "top", React.Dispatch<React.SetStateAction<"new" | "top">>] (+1 overload)useState(props: {
comments: inferInfiniteQueryHandle<typeof serverTRPC.post.comments>;
}
props.comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments.TRPCInfiniteQueryHandle<{ input: { postId: string; sort: "new" | "top"; cursor?: number | null | undefined; }; output: { items: { id: string; text: string; }[]; nextCursor: number | null; }; transformer: false; errorShape: DefaultErrorShape; featureFlags: { ...; }; }>.input: Omit<{
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
}, "cursor" | "direction">
input.sort: "new" | "top"sort);
const const commentsQuery: UseSuspenseInfiniteQueryResult<TRPCInfiniteData<{
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>>
commentsQuery = useSuspenseInfiniteQuery<{
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, TRPCInfiniteData<{
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>, TRPCQueryKeyWithoutPrefix, number | null>(options: UseSuspenseInfiniteQueryOptions<{
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, TRPCInfiniteData<...>, TRPCQueryKeyWithoutPrefix, number | null>, queryClient?: QueryClient): UseSuspenseInfiniteQueryResult<...>
useSuspenseInfiniteQuery(
const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
comments: QueryProcedure<{
input: {
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
comments: QueryProcedure<{
input: {
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments.DecorateInfiniteQueryProcedure<{ input: { postId: string; sort: "new" | "top"; cursor?: number | null | undefined; }; output: { items: { id: string; text: string; }[]; nextCursor: number | null; }; transformer: false; errorShape: DefaultErrorShape; featureFlags: { ...; }; }>.infiniteQueryOptions: TRPCInfiniteQueryOptions
<{
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>(input: {
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
}, opts: UnusedSkipTokenTRPCInfiniteQueryOptionsIn<{
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
...;
}>) => UnusedSkipTokenTRPCInfiniteQueryOptionsOut<...> (+2 overloads)
infiniteQueryOptions(
{ ...props: {
comments: inferInfiniteQueryHandle<typeof serverTRPC.post.comments>;
}
props.comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments.TRPCInfiniteQueryHandle<{ input: { postId: string; sort: "new" | "top"; cursor?: number | null | undefined; }; output: { items: { id: string; text: string; }[]; nextCursor: number | null; }; transformer: false; errorShape: DefaultErrorShape; featureFlags: { ...; }; }>.input: Omit<{
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
}, "cursor" | "direction">
input, sort: "new" | "top"sort },
{ getNextPageParam: GetNextPageParamFunction<number | null, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>
getNextPageParam: (last: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}
last) => last: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}
last.nextCursor: number | nullnextCursor },
),
);
return (
<>
<React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() => const setSort: (value: React.SetStateAction<"new" | "top">) => voidsetSort('top')}>Top</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>
<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>{const commentsQuery: UseSuspenseInfiniteQueryResult<TRPCInfiniteData<{
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>>
commentsQuery.data: TRPCInfiniteData<{
postId: string;
sort: "new" | "top";
cursor?: number | null | undefined;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>
data.InfiniteData<{ items: { id: string; text: string; }[]; nextCursor: number | null; }, number | null>.pages: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}[]
pages[0]?.items: {
id: string;
text: string;
}[]
items.Array<{ id: string; text: string; }>.length: numberlength} comments</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>
</>
);
}- While the input matches the handle's,
infiniteQueryOptionsreturns the same query key, so the first render reads the loader's cache entry instead of fetching again. For a query handle, useuseQuery(trpc.x.queryOptions({ ...handle.input, term }))the same way. - When the input changes, the new query starts when the component renders with it: its first page is a waterfall. Wrap
setSortinstartTransitionto keep showing the old list while it loads.
Request waterfalls
A request waterfall is a request that can't start until another one has finished. Each level costs a full round trip, and on a slow connection a round trip can take hundreds of milliseconds. Removing waterfalls is often the best way to speed up a page.
Handles are built around this. Queries start in loaders and server components, where everything a page needs is known up front, so they start together:
1. |-> post.byId
1. |-> post.comments
1. |-> post.moreByAuthor
instead of one after the other, as each component renders and finds out what it needs:
1. |-> post.byId
2. |-> post.comments
3. |-> post.byAuthor
The demo runs one blog both ways. Watch the network panel:
The route loader starts every query from the URL: the article's id, or the list's page. One round trip.
Comments
No requests yet.
To keep it that way:
- Load handles in loaders and server components, not in render.
- List every query in one load. See Waiting for some, streaming others.
- Make the URL the input, so the loader can start every query. See Start every query from the URL.
Dependent queries
A query whose input comes from another query's result is a waterfall by construction. Most pages don't need one: put the input in the route, or reshape the procedure so the page can start it from the URL. For example, trpc.post.moreByAuthor({ postId }) instead of trpc.post.byId and then trpc.post.byAuthor({ authorId }).
When the data really can't be reshaped, for example a third-party API you don't control, there are two explicit ways. Both pay the extra round trip.
In a loader or server component
Load the first query, then build the second load's entries from its data:
const [const first: HandlesOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
first, { const post: Postpost }] = await loadHandles<FeatureFlags, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>(trpc: DecorateRouterKeyable<...>, entries: {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}): Promise<...>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
post: ensureEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
ensureEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid }),
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
comments: prefetchInfiniteEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
...;
}, opts?: TRPCInfiniteEntryOptions<...> | undefined): TRPCInfiniteQueryEntry<...>
prefetchInfiniteEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments, { postId: stringpostId: const id: stringid }),
});
const [const second: HandlesOf<{
moreByAuthor: TRPCInfiniteQueryEntry<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
second] = await loadHandles<FeatureFlags, {
moreByAuthor: TRPCInfiniteQueryEntry<{
input: {
authorId: string;
cursor?: number | null | undefined;
};
output: {
items: Post[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>(trpc: DecorateRouterKeyable<FeatureFlags>, entries: {
moreByAuthor: TRPCInfiniteQueryEntry<{
input: {
authorId: string;
cursor?: number | null | undefined;
};
output: {
items: Post[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}): Promise<...>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
moreByAuthor: TRPCInfiniteQueryEntry<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
moreByAuthor: prefetchInfiniteEntry<{
input: {
authorId: string;
cursor?: number | null | undefined;
};
output: {
items: Post[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateInfiniteQueryProcedure<{
input: {
authorId: string;
cursor?: number | null | undefined;
};
output: {
items: Post[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
}, opts?: TRPCInfiniteEntryOptions<...> | undefined): TRPCInfiniteQueryEntry<...>
prefetchInfiniteEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byAuthor: DecorateQueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<...>
byAuthor, {
authorId: stringauthorId: const post: Postpost.authorId: stringauthorId,
}),
});
const const handles: {
moreByAuthor: TRPCInfiniteQueryHandle<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>;
post: TRPCQueryHandle<...>;
comments: TRPCInfiniteQueryHandle<...>;
}
handles = { ...const first: HandlesOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
first, ...const second: HandlesOf<{
moreByAuthor: TRPCInfiniteQueryEntry<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
second };The page waits for the post, and more by its author starts only after it.
In a component
When an input is only known at render time, such as an id from a parent query's result, read the query in the component that gets the input, as MoreByAuthor does in the post page at the top:
const const postsQuery: UseSuspenseInfiniteQueryResult<TRPCInfiniteData<{
authorId: string;
cursor?: number | null | undefined;
}, {
items: Post[];
nextCursor: number | null;
}>, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>>
postsQuery = useSuspenseInfiniteQuery<{
items: Post[];
nextCursor: number | null;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, TRPCInfiniteData<{
authorId: string;
cursor?: number | null | undefined;
}, {
items: Post[];
nextCursor: number | null;
}>, TRPCQueryKeyWithoutPrefix, number | null>(options: UseSuspenseInfiniteQueryOptions<{
items: Post[];
nextCursor: number | null;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, TRPCInfiniteData<...>, TRPCQueryKeyWithoutPrefix, number | null>, queryClient?: QueryClient): UseSuspenseInfiniteQueryResult<...>
useSuspenseInfiniteQuery(
const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byAuthor: DecorateQueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
...;
};
}> & DecorateInfiniteQueryProcedure<...>
byAuthor.DecorateInfiniteQueryProcedure<{ input: { authorId: string; cursor?: number | null | undefined; }; output: { items: Post[]; nextCursor: number | null; }; transformer: false; errorShape: DefaultErrorShape; featureFlags: { ...; }; }>.infiniteQueryOptions: TRPCInfiniteQueryOptions
<{
items: Post[];
nextCursor: number | null;
}, {
items: Post[];
nextCursor: number | null;
}>(input: {
authorId: string;
cursor?: number | null | undefined;
}, opts: UnusedSkipTokenTRPCInfiniteQueryOptionsIn<{
authorId: string;
cursor?: number | null | undefined;
}, {
items: Post[];
nextCursor: number | null;
}, {
items: Post[];
nextCursor: number | null;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
keyPrefix: false;
}>) => UnusedSkipTokenTRPCInfiniteQueryOptionsOut<...> (+2 overloads)
infiniteQueryOptions(
{ authorId: stringauthorId },
{ getNextPageParam: GetNextPageParamFunction<number | null, {
items: Post[];
nextCursor: number | null;
}>
getNextPageParam: (last: {
items: Post[];
nextCursor: number | null;
}
last) => last: {
items: Post[];
nextCursor: number | null;
}
last.nextCursor: number | nullnextCursor },
),
);- The query starts when
MoreByAuthorrenders, an extra round trip after its parent's data arrives. - In the Next.js App Router, load the dependent query in the server component instead, as in the loader example.
- In TanStack Router,
beforeLoadcan wait for the first query before the loader runs. It's an escape hatch, for when the dependent data must be there before the page renders: see TanStack Start › Handles inbeforeLoad.
React Router
React Router loads data in one of two places, and handles fit each a little differently. Route components pass handles from loaderData to the same client components as everywhere else.
Client-only apps
In SPA Mode (ssr: false) with clientLoader, or in Data Mode with createBrowserRouter, loaders run in the browser. Create the tab's QueryClient and proxy in a module, so loaders can import them, and provide the same client to your app:
import { class QueryClientQueryClient } from '@tanstack/react-query';
import { function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient, function httpBatchLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchLink } from '@trpc/client';
import { function createTRPCOptionsProxy<TRouter extends AnyRouter, TFeatureFlags extends FeatureFlags = {
keyPrefix: false;
}>(opts: TRPCOptionsProxyOptions<TRouter, TFeatureFlags>): TRPCOptionsProxy<TRouter, TFeatureFlags>
createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter } from '~/server/router';
// Only in a client-only app: with server rendering, every request would share it
export const const queryClient: QueryClientqueryClient = new new QueryClient(config?: QueryClientConfig): QueryClientQueryClient({
QueryClientConfig.defaultOptions?: DefaultOptions<Error> | undefineddefaultOptions: { DefaultOptions<Error>.queries?: OmitKeyof<QueryObserverOptions<unknown, Error, unknown, unknown, readonly unknown[], never>, "suspense" | "queryKey", "strictly"> | undefinedqueries: { staleTime?: StaleTimeFunction<unknown, Error, unknown, readonly unknown[]> | undefinedstaleTime: 30_000 } },
});
export const const trpcClient: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>
trpcClient = createTRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>(opts: CreateTRPCClientOptions<...>): TRPCClient<...>
createTRPCClient<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter>({
links: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>[]
links: [httpBatchLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>(opts: HTTPBatchLinkOptions<...>): TRPCLink<...>
httpBatchLink({ url: string | URLurl: '/api/trpc' })],
});
export const const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc = createTRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>(opts: TRPCOptionsProxyOptions<...>): TRPCOptionsProxy<...>
createTRPCOptionsProxy({ TRPCOptionsProxyOptionsExternal<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ viewer: QueryProcedure<{ input: void; output: { id: string; name: string; }; meta: object; errorShape: DefaultErrorShape; }>; post: BuiltRouter<...>; }>>>.client: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>> | TRPCUntypedClient<...>
client: const trpcClient: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>
trpcClient, queryClient: QueryClient | (() => QueryClient)queryClient });import { const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense } from 'react';
import { function Comments(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
Comments } from '~/components/comments';
import { function Post(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}): JSX.Element
Post } from '~/components/post';
import { const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc } from '~/trpc';
import type { Route } from './+types/post';
import { function loadHandles<TFeatureFlags extends FeatureFlags, TEntries extends Record<string, AnyTRPCEntry>>(trpc: DecorateRouterKeyable<TFeatureFlags>, entries: TEntries): Promise<TRPCLoadedHandles<TEntries>>loadHandles, function ensureEntry<TDef extends ResolverDef>(procedure: DecorateQueryProcedure<TDef>, ...args: EntryArgs<TDef>): TRPCQueryEntry<TDef, "ensure">ensureEntry, function prefetchInfiniteEntry<TDef extends ResolverDef>(procedure: DecorateInfiniteQueryProcedure<TDef>, input: TDef["input"], opts?: TRPCInfiniteEntryOptions<TDef>): TRPCInfiniteQueryEntry<TDef, "prefetch">prefetchInfiniteEntry } from '@trpc/tanstack-react-query/unstable-handles';
export async function function clientLoader({ params }: Route.ClientLoaderArgs): Promise<HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>>
clientLoader({ params: {
id: string;
}
params }: Route.type Route.ClientLoaderArgs = ClientDataFunctionArgs<{
id: string;
}> & {
serverLoader: () => Promise<undefined>;
}
ClientLoaderArgs) {
const [const handles: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
handles] = await loadHandles<FeatureFlags, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>(trpc: DecorateRouterKeyable<...>, entries: {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}): Promise<...>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
comments: prefetchInfiniteEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
...;
}, opts?: TRPCInfiniteEntryOptions<...> | undefined): TRPCInfiniteQueryEntry<...>
prefetchInfiniteEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments, { postId: stringpostId: params: {
id: string;
}
params.id: stringid, limit?: number | undefinedlimit: 20 }),
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
post: ensureEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
ensureEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid: params: {
id: string;
}
params.id: stringid }),
});
return const handles: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
handles;
}
export default function function PostPage({ loaderData }: Route.ComponentProps): JSX.ElementPostPage({ loaderData: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
loaderData }: Route.type Route.ComponentProps = {
params: {
id: string;
};
matches: [{
id: "routes/post";
params: Record<string, string | undefined>;
pathname: string;
loaderData: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>;
handle: unknown;
}, ...({
...;
} | undefined)[]];
} & {
...;
}
ComponentProps) {
return (
<>
<function Post(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}): JSX.Element
Post post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post={loaderData: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
loaderData.post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post} />
<const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.Attributes.key?: React.Key | null | undefinedkey={loaderData: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
loaderData.post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post.TRPCQueryHandle<{ input: { id: string; }; output: Post; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.input: {
id: string;
}
input.id: stringid} React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading comments…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}>
<function Comments(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
Comments comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments={loaderData: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
loaderData.comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense>
</>
);
}In Data Mode, the same function is a route object's loader, and the component reads it with useLoaderData:
import { const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense } from 'react';
import { function createBrowserRouter(routes: RouteObject[], opts?: DOMRouterOpts): RoutercreateBrowserRouter, function useLoaderData<T = any>(): SerializeFrom<T>useLoaderData, type interface LoaderFunctionArgs<Context = Readonly<RouterContextProvider>>LoaderFunctionArgs } from 'react-router';
import { function Comments(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
Comments } from '~/components/comments';
import { function Post(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}): JSX.Element
Post } from '~/components/post';
import { const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc } from '~/trpc';
import { function loadHandles<TFeatureFlags extends FeatureFlags, TEntries extends Record<string, AnyTRPCEntry>>(trpc: DecorateRouterKeyable<TFeatureFlags>, entries: TEntries): Promise<TRPCLoadedHandles<TEntries>>loadHandles, function ensureEntry<TDef extends ResolverDef>(procedure: DecorateQueryProcedure<TDef>, ...args: EntryArgs<TDef>): TRPCQueryEntry<TDef, "ensure">ensureEntry, function prefetchInfiniteEntry<TDef extends ResolverDef>(procedure: DecorateInfiniteQueryProcedure<TDef>, input: TDef["input"], opts?: TRPCInfiniteEntryOptions<TDef>): TRPCInfiniteQueryEntry<TDef, "prefetch">prefetchInfiniteEntry } from '@trpc/tanstack-react-query/unstable-handles';
async function function postLoader({ params }: LoaderFunctionArgs): Promise<HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>>
postLoader({ params: Params<string>params }: interface LoaderFunctionArgs<Context = Readonly<RouterContextProvider>>LoaderFunctionArgs) {
const const id: stringid = params: Params<string>params.string | undefinedid!;
const [const handles: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
handles] = await loadHandles<FeatureFlags, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>(trpc: DecorateRouterKeyable<...>, entries: {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}): Promise<...>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
comments: prefetchInfiniteEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
...;
}, opts?: TRPCInfiniteEntryOptions<...> | undefined): TRPCInfiniteQueryEntry<...>
prefetchInfiniteEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments, { postId: stringpostId: const id: stringid, limit?: number | undefinedlimit: 20 }),
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
post: ensureEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
ensureEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid }),
});
return const handles: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
handles;
}
function function PostPage(): JSX.ElementPostPage() {
const { const comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments, const post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post } = useLoaderData<({ params }: LoaderFunctionArgs) => Promise<HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>>>(): HandlesOf<...>
useLoaderData<typeof function postLoader({ params }: LoaderFunctionArgs): Promise<HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>>
postLoader>();
return (
<>
<function Post(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}): JSX.Element
Post post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post={const post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post} />
<const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.Attributes.key?: React.Key | null | undefinedkey={const post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post.TRPCQueryHandle<{ input: { id: string; }; output: Post; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.input: {
id: string;
}
input.id: stringid} React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading comments…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}>
<function Comments(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
Comments comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments={const comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense>
</>
);
}
export const const router: Routerrouter = function createBrowserRouter(routes: RouteObject[], opts?: DOMRouterOpts): RoutercreateBrowserRouter([
{ path?: string | undefinedpath: '/posts/:id', loader?: boolean | LoaderFunction<Readonly<RouterContextProvider>> | undefinedloader: function postLoader({ params }: LoaderFunctionArgs): Promise<HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>>
postLoader, Component?: React.ComponentType<{}> | null | undefinedComponent: function PostPage(): JSX.ElementPostPage },
]);- There is nothing to hydrate: the loader starts queries in the same
QueryClientthe components read. - The ensure entry waits on the first visit only. Coming back shows the cached post at once and refetches it in the background if it's stale.
- Key
Suspenseboundaries by the route's input. React Router renders the next page in a transition, and a transition waits for a boundary that's already showing content instead of showing its fallback. Without the key, navigating from one post to another waits for the comments.
Server rendering
In Framework Mode with server rendering, loader runs on the server, for the first page load and for every navigation after it. Create a query client and proxy per request in a root middleware. Loaders return dehydrate(queryClient) with their handles, and the route component wraps its children in TanStack Query's HydrationBoundary, as with any TanStack Query loader:
import type { class QueryClientQueryClient } from '@tanstack/react-query';
import {
function createTRPCOptionsProxy<TRouter extends AnyRouter, TFeatureFlags extends FeatureFlags = {
keyPrefix: false;
}>(opts: TRPCOptionsProxyOptions<TRouter, TFeatureFlags>): TRPCOptionsProxy<TRouter, TFeatureFlags>
createTRPCOptionsProxy,
type type TRPCOptionsProxy<TRouter extends AnyTRPCRouter, TFeatureFlags extends FeatureFlags = { keyPrefix: false; }> = DecoratedRouterRecord<TRouter["_def"]["_config"]["$types"], TRouter["_def"]["record"], TFeatureFlags> & DecorateRouterKeyable<TFeatureFlags>TRPCOptionsProxy,
} from '@trpc/tanstack-react-query';
import { function createContext<T>(defaultValue?: T): RouterContext<T>createContext } from 'react-router';
import { const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | null | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>
appRouter, type type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | null | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter } from '~/server/router';
import { const makeQueryClient: () => QueryClientmakeQueryClient } from './query-client';
export const const trpcContext: RouterContext<{
queryClient: QueryClient;
trpc: TRPCOptionsProxy<AppRouter>;
}>
trpcContext = createContext<{
queryClient: QueryClient;
trpc: TRPCOptionsProxy<AppRouter>;
}>(defaultValue?: {
queryClient: QueryClient;
trpc: TRPCOptionsProxy<AppRouter>;
} | undefined): RouterContext<{
queryClient: QueryClient;
trpc: TRPCOptionsProxy<AppRouter>;
}>
createContext<{
queryClient: QueryClientqueryClient: class QueryClientQueryClient;
trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | null | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>
trpc: type TRPCOptionsProxy<TRouter extends AnyTRPCRouter, TFeatureFlags extends FeatureFlags = { keyPrefix: false; }> = DecoratedRouterRecord<TRouter["_def"]["_config"]["$types"], TRouter["_def"]["record"], TFeatureFlags> & DecorateRouterKeyable<TFeatureFlags>TRPCOptionsProxy<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | null | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter>;
}>();
export function function createServerTRPC(): {
queryClient: QueryClient;
trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<...>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>;
}
createServerTRPC() {
const const queryClient: QueryClientqueryClient = function makeQueryClient(): QueryClientmakeQueryClient();
const const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc = createTRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>(opts: TRPCOptionsProxyOptions<...>): TRPCOptionsProxy<...>
createTRPCOptionsProxy({ TRPCOptionsProxyOptionsInternal<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ viewer: QueryProcedure<{ input: void; output: { id: string; name: string; }; meta: object; errorShape: DefaultErrorShape; }>; post: BuiltRouter<...>; }>>>.router: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>
router: const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>
appRouter, TRPCOptionsProxyOptionsInternal<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ viewer: QueryProcedure<{ input: void; output: { id: string; name: string; }; meta: object; errorShape: DefaultErrorShape; }>; post: BuiltRouter<...>; }>>>.ctx: object | (() => MaybePromise<object>)ctx: {}, queryClient: QueryClient | (() => QueryClient)queryClient });
return { queryClient: QueryClientqueryClient, trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc };
}import { const QueryClientProvider: ({ client, children }: QueryClientProviderProps) => React.JSX.ElementQueryClientProvider } from '@tanstack/react-query';
import { function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient } from '@trpc/client';
import { function useState<S>(initialState: S | (() => S)): [S, React.Dispatch<React.SetStateAction<S>>] (+1 overload)useState } from 'react';
import { function Outlet(props: OutletProps): React.ReactElement | nullOutlet } from 'react-router';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter } from '~/server/router';
import { const TRPCProvider: TRPCProviderType<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
TRPCProvider } from '~/trpc/client';
import type { Route } from './+types/root';
import { function makeQueryClient(): QueryClientmakeQueryClient } from './trpc/query-client';
import { function createServerTRPC(): {
queryClient: QueryClient;
trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<...>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>;
}
createServerTRPC, const trpcContext: RouterContext<{
queryClient: QueryClient;
trpc: TRPCOptionsProxy<AppRouter>;
}>
trpcContext } from './trpc/server';
export const const middleware: CreateServerMiddlewareFunction<Info & {
module: typeof import("./root");
matches: [{
id: "root";
module: typeof import("./root");
}];
}>[]
middleware: Route.type Route.MiddlewareFunction = (args: ServerDataFunctionArgs<{
[x: string]: string | undefined;
}>, next: MiddlewareNextFunction<Response>) => MaybePromise<void | Response>
MiddlewareFunction[] = [
({ context: Readonly<RouterContextProvider>context }) => {
context: Readonly<RouterContextProvider>context.set<RouterContext<{
queryClient: QueryClient;
trpc: TRPCOptionsProxy<AppRouter>;
}>>(context: RouterContext<{
queryClient: QueryClient;
trpc: TRPCOptionsProxy<AppRouter>;
}>, value: {
queryClient: QueryClient;
trpc: TRPCOptionsProxy<AppRouter>;
}): void
set(const trpcContext: RouterContext<{
queryClient: QueryClient;
trpc: TRPCOptionsProxy<AppRouter>;
}>
trpcContext, function createServerTRPC(): {
queryClient: QueryClient;
trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<...>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>;
}
createServerTRPC());
},
];
export default function function App(): JSX.ElementApp() {
const [const queryClient: QueryClientqueryClient] = useState<QueryClient>(initialState: QueryClient | (() => QueryClient)): [QueryClient, React.Dispatch<React.SetStateAction<QueryClient>>] (+1 overload)useState(function makeQueryClient(): QueryClientmakeQueryClient);
const [const trpcClient: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>
trpcClient] = useState<TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>>(initialState: TRPCClient<...> | (() => TRPCClient<...>)): [...] (+1 overload)
useState(() => createTRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>(opts: CreateTRPCClientOptions<...>): TRPCClient<...>
createTRPCClient<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter>({ links: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>[]
links: [/* ... */] }));
return (
<const QueryClientProvider: ({ client, children }: QueryClientProviderProps) => React.JSX.ElementQueryClientProvider client: QueryClientclient={const queryClient: QueryClientqueryClient}>
<const TRPCProvider: TRPCProviderType<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
TRPCProvider trpcClient: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>
trpcClient={const trpcClient: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>
trpcClient} queryClient: QueryClientqueryClient={const queryClient: QueryClientqueryClient}>
<function Outlet(props: OutletProps): React.ReactElement | nullOutlet />
</const TRPCProvider: TRPCProviderType<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
TRPCProvider>
</const QueryClientProvider: ({ client, children }: QueryClientProviderProps) => React.JSX.ElementQueryClientProvider>
);
}import { function dehydrate(client: QueryClient, options?: DehydrateOptions): DehydratedStatedehydrate, const HydrationBoundary: ({ children, options, state, queryClient }: HydrationBoundaryProps) => React.ReactElementHydrationBoundary } from '@tanstack/react-query';
import { function loadHandles<TFeatureFlags extends FeatureFlags, TEntries extends Record<string, AnyTRPCEntry>>(trpc: DecorateRouterKeyable<TFeatureFlags>, entries: TEntries): Promise<TRPCLoadedHandles<TEntries>>loadHandles, function ensureEntry<TDef extends ResolverDef>(procedure: DecorateQueryProcedure<TDef>, ...args: EntryArgs<TDef>): TRPCQueryEntry<TDef, "ensure">ensureEntry, function prefetchInfiniteEntry<TDef extends ResolverDef>(procedure: DecorateInfiniteQueryProcedure<TDef>, input: TDef["input"], opts?: TRPCInfiniteEntryOptions<TDef>): TRPCInfiniteQueryEntry<TDef, "prefetch">prefetchInfiniteEntry } from '@trpc/tanstack-react-query/unstable-handles';
import { const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense } from 'react';
import { function Comments(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
Comments } from '~/components/comments';
import { function Post(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}): JSX.Element
Post } from '~/components/post';
import { const trpcContext: RouterContext<{
queryClient: QueryClient;
trpc: TRPCOptionsProxy<AppRouter>;
}>
trpcContext } from '../trpc/server';
import type { Route } from './+types/post';
export async function function loader({ params, context }: Route.LoaderArgs): Promise<{
comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>;
post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>;
dehydratedState: DehydratedState;
}>
loader({ params: {
id: string;
}
params, context: Readonly<RouterContextProvider>context }: Route.type Route.LoaderArgs = {
request: Request;
url: URL;
params: {
id: string;
};
pattern: string;
context: Readonly<RouterContextProvider>;
}
LoaderArgs) {
const { const queryClient: QueryClientqueryClient, const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>
trpc } = context: Readonly<RouterContextProvider>context.get<{
queryClient: QueryClient;
trpc: TRPCOptionsProxy<AppRouter>;
}>(context: RouterContext<{
queryClient: QueryClient;
trpc: TRPCOptionsProxy<AppRouter>;
}>): {
queryClient: QueryClient;
trpc: TRPCOptionsProxy<AppRouter>;
}
get(const trpcContext: RouterContext<{
queryClient: QueryClient;
trpc: TRPCOptionsProxy<AppRouter>;
}>
trpcContext);
const [const handles: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
handles] = await loadHandles<FeatureFlags, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>(trpc: DecorateRouterKeyable<...>, entries: {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}): Promise<...>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>
trpc, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
comments: prefetchInfiniteEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
...;
}, opts?: TRPCInfiniteEntryOptions<...> | undefined): TRPCInfiniteQueryEntry<...>
prefetchInfiniteEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments, { postId: stringpostId: params: {
id: string;
}
params.id: stringid, limit?: number | undefinedlimit: 20 }),
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
post: ensureEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
ensureEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid: params: {
id: string;
}
params.id: stringid }),
});
return { dehydratedState: DehydratedStatedehydratedState: function dehydrate(client: QueryClient, options?: DehydrateOptions): DehydratedStatedehydrate(const queryClient: QueryClientqueryClient), ...const handles: HandlesOf<{
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
handles };
}
export default function function PostPage({ loaderData }: Route.ComponentProps): JSX.ElementPostPage({ loaderData: {
comments: {
readonly queryKey: [path: string[], opts?: {
input?: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction"> | undefined;
type?: "infinite" | undefined;
} | undefined];
readonly input: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction">;
readonly initialCursor?: number | null | undefined;
readonly [handleDef]: {
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: {
...;
};
featureFlags: {
...;
};
kind: "infinite";
};
};
post: {
...;
};
dehydratedState: {
...;
};
}
loaderData }: Route.type Route.ComponentProps = {
params: {
id: string;
};
matches: [{
id: "routes/post";
params: Record<string, string | undefined>;
pathname: string;
loaderData: {
comments: {
readonly queryKey: [path: string[], opts?: {
input?: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction"> | undefined;
type?: "infinite" | undefined;
} | undefined];
readonly input: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction">;
readonly initialCursor?: number | null | undefined;
readonly [handleDef]: {
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: {
...;
};
featureFlags: {
...;
};
kind: "infinite";
};
};
post: {
...;
};
dehydratedState: {
...;
};
};
handle: unknown;
}, ...({
...;
} | undefined)[]];
} & {
...;
}
ComponentProps) {
return (
<const HydrationBoundary: ({ children, options, state, queryClient }: HydrationBoundaryProps) => React.ReactElementHydrationBoundary HydrationBoundaryProps.state: DehydratedState | null | undefinedstate={loaderData: {
comments: {
readonly queryKey: [path: string[], opts?: {
input?: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction"> | undefined;
type?: "infinite" | undefined;
} | undefined];
readonly input: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction">;
readonly initialCursor?: number | null | undefined;
readonly [handleDef]: {
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: {
...;
};
featureFlags: {
...;
};
kind: "infinite";
};
};
post: {
...;
};
dehydratedState: {
...;
};
}
loaderData.dehydratedState: {
mutations: {
mutationKey?: readonly undefined[] | undefined;
state: {
context: undefined;
data: undefined;
error: Error | null;
failureCount: number;
failureReason: Error | null;
isPaused: boolean;
status: MutationStatus;
variables: undefined;
submittedAt: number;
};
meta?: {
[x: string]: undefined;
} | undefined;
scope?: MutationScope | undefined;
}[];
queries: {
queryHash: string;
queryKey: readonly undefined[];
state: {
data: undefined;
dataUpdateCount: number;
dataUpdatedAt: number;
error: Error | null;
errorUpdateCount: number;
... 6 more ...;
fetchStatus: FetchStatus;
};
dehydratedAt: number;
promise?: Promise<...> | undefined;
meta?: {
[x: string]: undefined;
} | undefined;
queryType?: "infinite" | undefined;
}[];
}
dehydratedState}>
<function Post(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}): JSX.Element
Post post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
...;
};
}>
post={loaderData: {
comments: {
readonly queryKey: [path: string[], opts?: {
input?: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction"> | undefined;
type?: "infinite" | undefined;
} | undefined];
readonly input: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction">;
readonly initialCursor?: number | null | undefined;
readonly [handleDef]: {
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: {
...;
};
featureFlags: {
...;
};
kind: "infinite";
};
};
post: {
...;
};
dehydratedState: {
...;
};
}
loaderData.post: {
readonly queryKey: [path: string[], opts?: {
input?: {
id: string;
} | undefined;
type?: "query" | undefined;
} | undefined];
readonly input: {
id: string;
};
readonly [handleDef]: {
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: {
...;
};
featureFlags: {
...;
};
kind: "query";
};
}
post} />
<const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.Attributes.key?: React.Key | null | undefinedkey={loaderData: {
comments: {
readonly queryKey: [path: string[], opts?: {
input?: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction"> | undefined;
type?: "infinite" | undefined;
} | undefined];
readonly input: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction">;
readonly initialCursor?: number | null | undefined;
readonly [handleDef]: {
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: {
...;
};
featureFlags: {
...;
};
kind: "infinite";
};
};
post: {
...;
};
dehydratedState: {
...;
};
}
loaderData.post: {
readonly queryKey: [path: string[], opts?: {
input?: {
id: string;
} | undefined;
type?: "query" | undefined;
} | undefined];
readonly input: {
id: string;
};
readonly [handleDef]: {
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: {
...;
};
featureFlags: {
...;
};
kind: "query";
};
}
post.input: {
id: string;
}
input.id: stringid} React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading comments…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}>
<function Comments(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
Comments comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
...;
};
}>
comments={loaderData: {
comments: {
readonly queryKey: [path: string[], opts?: {
input?: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction"> | undefined;
type?: "infinite" | undefined;
} | undefined];
readonly input: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction">;
readonly initialCursor?: number | null | undefined;
readonly [handleDef]: {
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: {
...;
};
featureFlags: {
...;
};
kind: "infinite";
};
};
post: {
...;
};
dehydratedState: {
...;
};
}
loaderData.comments: {
readonly queryKey: [path: string[], opts?: {
input?: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction"> | undefined;
type?: "infinite" | undefined;
} | undefined];
readonly input: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction">;
readonly initialCursor?: number | null | undefined;
readonly [handleDef]: {
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: {
...;
};
featureFlags: {
...;
};
kind: "infinite";
};
}
comments} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense>
</const HydrationBoundary: ({ children, options, state, queryClient }: HydrationBoundaryProps) => React.ReactElementHydrationBoundary>
);
}makeQueryClientis the query client factory from the server components guide: it dehydrates pending queries and runs your transformer'sserializeanddeserialize.- React Router streams promises in loader data, so the comments' pending query crosses as a promise inside
dehydratedState. The page's HTML doesn't wait for it, and the browser doesn't fetch it again. dehydrate(queryClient)sends everything the request's client holds. Nested routes share it, so a query another loader started can be sent twice. Filter withdehydrate'sshouldDehydrateQueryif that matters.- Nested routes share the request's proxy, since the middleware runs once per request. A query that two loaders start is requested once.
- In a
loader, an ensure entry waits like a fetch entry, since the request'sQueryClientstarts empty. Use a prefetch entry for queries the page can stream.
Next.js App Router
This builds on the Next.js App Router setup. The query client factory, getQueryClient and the server proxy in trpc/server.tsx stay as they are. Add export type TRPC = TRPCOptionsProxy<AppRouter> to trpc/client.tsx, and in trpc/server.tsx, replace the setup's HydrateClient with one from unstable_createHydrateClient:
import { function unstable_createHydrateClient(_queryClient: QueryClient | (() => QueryClient)): React.FC<unstable_HydrateClientProps>unstable_createHydrateClient } from '@trpc/tanstack-react-query';
// ... `getQueryClient` and the `trpc` proxy as before
export const const HydrateClient: React.FC<unstable_HydrateClientProps>HydrateClient = function unstable_createHydrateClient(_queryClient: QueryClient | (() => QueryClient)): React.FC<unstable_HydrateClientProps>unstable_createHydrateClient(const getQueryClient: () => QueryClientgetQueryClient);The client components are the same either way: they read the handle with useSuspenseQuery or useQuery, as in Typing props.
HydrateClient
HydrateClient sends the queries the request has started so far to the browser, including pending ones, which stream in as they settle. Render one in each server component that starts queries for client components:
import { const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense } from 'react';
import { const HydrateClient: React.FC<unstable_HydrateClientProps>HydrateClient, const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc } from '~/trpc/server';
import { function Comments(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
Comments } from './comments';
import { function Post(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}): JSX.Element
Post } from './post';
import { function loadHandles<TFeatureFlags extends FeatureFlags, TEntries extends Record<string, AnyTRPCEntry>>(trpc: DecorateRouterKeyable<TFeatureFlags>, entries: TEntries): Promise<TRPCLoadedHandles<TEntries>>loadHandles, function prefetchEntry<TDef extends ResolverDef>(procedure: DecorateQueryProcedure<TDef>, ...args: EntryArgs<TDef>): TRPCQueryEntry<TDef, "prefetch">prefetchEntry, function prefetchInfiniteEntry<TDef extends ResolverDef>(procedure: DecorateInfiniteQueryProcedure<TDef>, input: TDef["input"], opts?: TRPCInfiniteEntryOptions<TDef>): TRPCInfiniteQueryEntry<TDef, "prefetch">prefetchInfiniteEntry } from '@trpc/tanstack-react-query/unstable-handles';
export default async function function PostPage(props: {
params: Promise<{
id: string;
}>;
}): Promise<JSX.Element>
PostPage(props: {
params: Promise<{
id: string;
}>;
}
props: { params: Promise<{
id: string;
}>
params: interface Promise<T>Promise<{ id: stringid: string }> }) {
const { const id: stringid } = await props: {
params: Promise<{
id: string;
}>;
}
props.params: Promise<{
id: string;
}>
params;
const [{ const post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post }] = await loadHandles<FeatureFlags, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>(trpc: DecorateRouterKeyable<FeatureFlags>, entries: {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}): Promise<TRPCLoadedHandles<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
post: prefetchEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
prefetchEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid }),
});
return (
<const HydrateClient: React.FC<unstable_HydrateClientProps>HydrateClient>
<const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading post…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}>
<function Post(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}): JSX.Element
Post post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post={const post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense>
<const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading comments…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}>
<function CommentsSection(props: {
id: string;
}): Promise<JSX.Element>
CommentsSection id: stringid={const id: stringid} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense>
</const HydrateClient: React.FC<unstable_HydrateClientProps>HydrateClient>
);
}
async function function CommentsSection(props: {
id: string;
}): Promise<JSX.Element>
CommentsSection(props: {
id: string;
}
props: { id: stringid: string }) {
const [{ const comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments }] = await loadHandles<FeatureFlags, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>(trpc: DecorateRouterKeyable<FeatureFlags>, entries: {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}): Promise<...>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
comments: prefetchInfiniteEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
...;
}, opts?: TRPCInfiniteEntryOptions<...> | undefined): TRPCInfiniteQueryEntry<...>
prefetchInfiniteEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments, { postId: stringpostId: props: {
id: string;
}
props.id: stringid, limit?: number | undefinedlimit: 20 }),
});
return (
<const HydrateClient: React.FC<unstable_HydrateClientProps>HydrateClient>
<function Comments(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
Comments comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments={const comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments} />
</const HydrateClient: React.FC<unstable_HydrateClientProps>HydrateClient>
);
}- A query an earlier
HydrateClientsent in the same request isn't sent again, so nesting them is fine. The outer one renders beforeCommentsSectionstarts its query, so each sends only its own. - A query is sent again when the server has newer data for it, for example after a fetch entry refetched it.
- It keeps your query client's
dehydrateoptions:shouldDehydrateQuery,serializeDataand the rest. - It works only in server components.
@trpc/tanstack-react-queryresolves to its server build through thereact-serverexport condition, which Next.js and other RSC bundlers set for server components. Rendered anywhere else, it throws. - It works for queries started with
queryClient.prefetchQuery(trpc.x.queryOptions(input))too, not only handles.
Streaming with prefetch entries
The page sends its HTML at once. Each Suspense boundary shows its fallback, then the data streams in:
import { const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense } from 'react';
import { const HydrateClient: React.FC<unstable_HydrateClientProps>HydrateClient, const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc } from '~/trpc/server';
import { function Comments(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
Comments } from './comments';
import { function Post(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}): JSX.Element
Post } from './post';
import { function loadHandles<TFeatureFlags extends FeatureFlags, TEntries extends Record<string, AnyTRPCEntry>>(trpc: DecorateRouterKeyable<TFeatureFlags>, entries: TEntries): Promise<TRPCLoadedHandles<TEntries>>loadHandles, function prefetchEntry<TDef extends ResolverDef>(procedure: DecorateQueryProcedure<TDef>, ...args: EntryArgs<TDef>): TRPCQueryEntry<TDef, "prefetch">prefetchEntry, function prefetchInfiniteEntry<TDef extends ResolverDef>(procedure: DecorateInfiniteQueryProcedure<TDef>, input: TDef["input"], opts?: TRPCInfiniteEntryOptions<TDef>): TRPCInfiniteQueryEntry<TDef, "prefetch">prefetchInfiniteEntry } from '@trpc/tanstack-react-query/unstable-handles';
export default async function function PostPage(props: {
params: Promise<{
id: string;
}>;
}): Promise<JSX.Element>
PostPage(props: {
params: Promise<{
id: string;
}>;
}
props: {
params: Promise<{
id: string;
}>
params: interface Promise<T>Promise<{ id: stringid: string }>;
}) {
const { const id: stringid } = await props: {
params: Promise<{
id: string;
}>;
}
props.params: Promise<{
id: string;
}>
params;
const [{ const post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post, const comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments }] = await loadHandles<FeatureFlags, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>(trpc: DecorateRouterKeyable<...>, entries: {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}): Promise<...>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
post: prefetchEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
prefetchEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid }),
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
comments: prefetchInfiniteEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
...;
}, opts?: TRPCInfiniteEntryOptions<...> | undefined): TRPCInfiniteQueryEntry<...>
prefetchInfiniteEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments, { postId: stringpostId: const id: stringid, limit?: number | undefinedlimit: 20 }),
});
return (
<const HydrateClient: React.FC<unstable_HydrateClientProps>HydrateClient>
<const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading post…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}>
<function Post(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}): JSX.Element
Post post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post={const post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense>
<const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading comments…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}>
<function Comments(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
Comments comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments={const comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense>
</const HydrateClient: React.FC<unstable_HydrateClientProps>HydrateClient>
);
}- Give each handle its own
Suspenseboundary, so one slow query doesn't hold back the rest. - If the post doesn't exist, the
Postcomponent throws, and the nearesterror.tsxshows the error.
Waiting with ensure entries
The page waits for the post before it renders. Use this when the page can't render without the data, or when you need it on the server, for example to call notFound():
import {
function ensureEntry<TDef extends ResolverDef>(procedure: DecorateQueryProcedure<TDef>, ...args: EntryArgs<TDef>): TRPCQueryEntry<TDef, "ensure">ensureEntry,
function isTRPCEntryError<TDef extends ResolverDef>(error: unknown, procedure: DecorateQueryProcedure<TDef>): error is TRPCEntryError<TDef> (+1 overload)isTRPCEntryError,
function loadHandles<TFeatureFlags extends FeatureFlags, TEntries extends Record<string, AnyTRPCEntry>>(trpc: DecorateRouterKeyable<TFeatureFlags>, entries: TEntries): Promise<TRPCLoadedHandles<TEntries>>loadHandles,
} from '@trpc/tanstack-react-query/unstable-handles';
import { function notFound(): nevernotFound } from 'next/navigation';
import { const HydrateClient: FC<unstable_HydrateClientProps>HydrateClient, const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc } from '~/trpc/server';
import { function Post(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}): JSX.Element
Post } from './post';
export default async function function PostPage(props: {
params: Promise<{
id: string;
}>;
}): Promise<JSX.Element>
PostPage(props: {
params: Promise<{
id: string;
}>;
}
props: {
params: Promise<{
id: string;
}>
params: interface Promise<T>Promise<{ id: stringid: string }>;
}) {
const { const id: stringid } = await props: {
params: Promise<{
id: string;
}>;
}
props.params: Promise<{
id: string;
}>
params;
const [{ const post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post }] = await loadHandles<FeatureFlags, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>(trpc: DecorateRouterKeyable<FeatureFlags>, entries: {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}): Promise<TRPCLoadedHandles<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
post: ensureEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
ensureEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid }),
}).Promise<TRPCLoadedHandles<{ post: TRPCQueryEntry<{ input: { id: string; }; output: Post; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }, "ensure">; }>>.catch<never>(onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<TRPCLoadedHandles<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>>
catch((cause: anycause) => {
if (isTRPCEntryError<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(error: unknown, procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>): error is TRPCEntryError<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> (+1 overload)
isTRPCEntryError(cause: anycause, const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId) && cause: TRPCEntryError<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
cause.TRPCEntryError<{ input: { id: string; }; output: Post; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.code: "PARSE_ERROR" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST" | undefinedcode === 'NOT_FOUND') {
function notFound(): nevernotFound();
}
throw cause: anycause;
});
return (
<const HydrateClient: FC<unstable_HydrateClientProps>HydrateClient>
<function Post(props: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
}): JSX.Element
Post post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post={const post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post} />
</const HydrateClient: FC<unstable_HydrateClientProps>HydrateClient>
);
}- The data is in the cache before
Postrenders, so it shows the post at once. If the cache loses the post later,Postsuspends and shows the nearestloading.tsxorSuspensefallback: see When the cache loses the data. - To use the data on the server, read it from the cache: see Using the data in a loader.
To wait for some queries and stream others on the same page, see Combining both.
Handle inputs
Handle inputs cross to the client inside the handle, through React's own serialization, not your tRPC data transformer. This isn't new: dehydrated query keys already cross the same way. Keep inputs to plain JSON values and Dates. React can send a Map, Set or BigInt too, but TanStack Query hashes query keys with JSON.stringify: a BigInt input throws, and two different Map or Set inputs share one cache entry.
Errors
With ensure and fetch entries
When an ensure or fetch entry fails, loadHandles rejects with a TRPCEntryError that says which entry it was:
error.key: the entry's key, such as'post'.error.path: the procedure, such as'post.byId'.error.code: the tRPC error code, such as'NOT_FOUND'.error.cause: the request's error. That's aTRPCErroron the server proxy, and aTRPCClientErroron a client-backed proxy, such as one in a TanStack Start loader.
isTRPCEntryError(error, trpc.post.byId) checks that the failed entry was for that procedure, and types error.cause with its errors. In the post page at the top, only the post is awaited, so only the post can make the load reject. If the page waits for the comments too, chain .catch to check which one failed, so a missing post gets a 404:
import {
function ensureEntry<TDef extends ResolverDef>(procedure: DecorateQueryProcedure<TDef>, ...args: EntryArgs<TDef>): TRPCQueryEntry<TDef, "ensure">ensureEntry,
function ensureInfiniteEntry<TDef extends ResolverDef>(procedure: DecorateInfiniteQueryProcedure<TDef>, input: TDef["input"], opts?: TRPCInfiniteEntryOptions<TDef>): TRPCInfiniteQueryEntry<TDef, "ensure">ensureInfiniteEntry,
function isTRPCEntryError<TDef extends ResolverDef>(error: unknown, procedure: DecorateQueryProcedure<TDef>): error is TRPCEntryError<TDef> (+1 overload)isTRPCEntryError,
function loadHandles<TFeatureFlags extends FeatureFlags, TEntries extends Record<string, AnyTRPCEntry>>(trpc: DecorateRouterKeyable<TFeatureFlags>, entries: TEntries): Promise<TRPCLoadedHandles<TEntries>>loadHandles,
} from '@trpc/tanstack-react-query/unstable-handles';
async function function loader(id: string): Promise<HandlesOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>>
loader(id: stringid: string) {
const [const handles: HandlesOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
handles] = await loadHandles<FeatureFlags, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>(trpc: DecorateRouterKeyable<...>, entries: {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}): Promise<...>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
post: ensureEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
ensureEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid }),
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
comments: ensureInfiniteEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
...;
}, opts?: TRPCInfiniteEntryOptions<...> | undefined): TRPCInfiniteQueryEntry<...>
ensureInfiniteEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments, { postId: stringpostId: id: stringid }),
}).Promise<TRPCLoadedHandles<{ post: TRPCQueryEntry<{ input: { id: string; }; output: Post; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }, "ensure">; comments: TRPCInfiniteQueryEntry<...>; }>>.catch<never>(onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<TRPCLoadedHandles<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>>
catch((error: unknownerror: unknown) => {
if (isTRPCEntryError<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(error: unknown, procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>): error is TRPCEntryError<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> (+1 overload)
isTRPCEntryError(error: unknownerror, const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId) && error: TRPCEntryError<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
error.TRPCEntryError<{ input: { id: string; }; output: Post; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.code: "PARSE_ERROR" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST" | undefinedcode === 'NOT_FOUND') {
function notFound(): nevernotFound();
}
if (isTRPCEntryError<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(error: unknown, procedure: DecorateQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>): error is TRPCEntryError<...> (+1 overload)
isTRPCEntryError(error: unknownerror, const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments)) {
function reportError(error: unknown): voidreportError(error.TRPCEntryError<{ input: { postId: string; limit?: number | undefined; cursor?: number | null | undefined; }; output: { items: { id: string; text: string; }[]; nextCursor: number | null; }; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.cause: TRPCError | ErrorOf<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
cause); }
throw error: unknownerror;
});
return const handles: HandlesOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
}>
handles;
}- The load rejects as soon as one entry fails, without waiting for the others. If several fail, you get the first to fail.
- Errors that don't come from the request, such as a cancelled query, reject as they are.
Errors as values
loadHandlesSettled takes the same entries, but never rejects because an entry failed. It waits for every fetch and ensure entry to settle, then resolves to [handles, results]. Each of those entries has { status: 'success', data } or { status: 'error', error } on results, with a TRPCEntryError:
import {
function ensureEntry<TDef extends ResolverDef>(procedure: DecorateQueryProcedure<TDef>, ...args: EntryArgs<TDef>): TRPCQueryEntry<TDef, "ensure">ensureEntry,
function loadHandlesSettled<TFeatureFlags extends FeatureFlags, TEntries extends Record<string, AnyTRPCEntry>>(trpc: DecorateRouterKeyable<TFeatureFlags>, entries: TEntries): Promise<TRPCSettledHandles<TEntries>>loadHandlesSettled,
function prefetchInfiniteEntry<TDef extends ResolverDef>(procedure: DecorateInfiniteQueryProcedure<TDef>, input: TDef["input"], opts?: TRPCInfiniteEntryOptions<TDef>): TRPCInfiniteQueryEntry<TDef, "prefetch">prefetchInfiniteEntry,
} from '@trpc/tanstack-react-query/unstable-handles';
const [const handles: HandlesOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
handles, const results: ResultsOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
results] = await loadHandlesSettled<FeatureFlags, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>(trpc: DecorateRouterKeyable<...>, entries: {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}): Promise<...>
loadHandlesSettled(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
post: ensureEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">
ensureEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid }),
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
comments: prefetchInfiniteEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
...;
}, opts?: TRPCInfiniteEntryOptions<...> | undefined): TRPCInfiniteQueryEntry<...>
prefetchInfiniteEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments, { postId: stringpostId: const id: stringid }),
});
if (const results: ResultsOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
results.post: TRPCEntryResult<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, Post>
post.status: "error" | "success"status === 'error') {
if (const results: ResultsOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
results.post: {
readonly status: "error";
readonly error: TRPCEntryError<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>;
}
post.error: TRPCEntryError<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
error.TRPCEntryError<{ input: { id: string; }; output: Post; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.code: "PARSE_ERROR" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST" | undefinedcode === 'NOT_FOUND') {
function notFound(): nevernotFound();
}
throw const results: ResultsOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
results.post: {
readonly status: "error";
readonly error: TRPCEntryError<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>;
}
post.error: TRPCEntryError<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
error;
}
const results: ResultsOf<{
post: TRPCQueryEntry<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "ensure">;
comments: TRPCInfiniteQueryEntry<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>
results.post: {
readonly status: "success";
readonly data: Post;
}
post.data.title: stringtitle;- It waits for every fetch and ensure entry, even after one has failed, so you see all their failures, not just the first.
resultshas no key for prefetch entries, asdatahasn't withloadHandles.handleshas a handle for every entry, failed or not. A component that reads a failed one gets the error, as with a prefetch entry.- A cancelled query still rejects, as with
loadHandles: the load was called off, so there's no result to give.
With prefetch entries
A prefetch entry never makes the load reject. The component that reads the handle gets the error instead: useQuery returns it as status: 'error', unless you pass throwOnError, and useSuspenseQuery throws it to the nearest error boundary unless it has cached data to show. It gets the query's own error, not a TRPCEntryError: the boundary already knows which handle it wraps.
import { function isTRPCClientError<TInferrable extends InferrableClientTypes>(cause: unknown): cause is TRPCClientError<TInferrable>isTRPCClientError } from '@trpc/client';
import { const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense } from 'react';
import { function ErrorBoundary(props: {
fallbackRender: (props: {
error: unknown;
}) => React.ReactNode;
children: React.ReactNode;
}): React.ReactNode
ErrorBoundary } from 'react-error-boundary';
import { function Comments(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
Comments } from '~/components/comments';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter } from '~/server/router';
function function CommentsError(props: {
error: unknown;
}): JSX.Element
CommentsError(props: {
error: unknown;
}
props: { error: unknownerror: unknown }) {
if (isTRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>(cause: unknown): cause is TRPCClientError<...>
isTRPCClientError<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter>(props: {
error: unknown;
}
props.error: unknownerror)) {
return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Couldn't load the comments ({props: {
error: unknown;
}
props.error: TRPCClientError<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>>>
error.TRPCClientError<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ viewer: QueryProcedure<{ input: void; output: { id: string; name: string; }; meta: object; errorShape: DefaultErrorShape; }>; post: BuiltRouter<...>; }>>>.data: Maybe<DefaultErrorData>data?.code: "PARSE_ERROR" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_SUPPORTED" | "TIMEOUT" | "CONFLICT" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_CONTENT" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST" | undefinedcode}).</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>;
}
throw props: {
error: unknown;
}
props.error: unknownerror;
}
export function function PostComments(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
PostComments(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}
props: {
comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments: type inferInfiniteQueryHandle<TProcedure extends DecorateInfiniteQueryProcedure<any>> = TProcedure extends DecorateInfiniteQueryProcedure<infer TDef extends ResolverDef> ? TRPCInfiniteQueryHandle<TDef> : neverinferInfiniteQueryHandle<type TRPC = DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
viewer: QueryProcedure<{
input: void;
output: {
id: string;
name: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<...>;
addComment: MutationProcedure<...>;
}>>;
}>, {
...;
}> & DecorateRouterKeyable<...>
TRPC['post']['comments']>;
}) {
return (
<function ErrorBoundary(props: {
fallbackRender: (props: {
error: unknown;
}) => React.ReactNode;
children: React.ReactNode;
}): React.ReactNode
ErrorBoundary fallbackRender: (props: {
error: unknown;
}) => React.ReactNode
fallbackRender={function CommentsError(props: {
error: unknown;
}): JSX.Element
CommentsError}>
<const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading comments…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}>
<function Comments(props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
Comments comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments={props: {
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}
props.comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
};
output: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense>
</function ErrorBoundary(props: {
fallbackRender: (props: {
error: unknown;
}) => React.ReactNode;
children: React.ReactNode;
}): React.ReactNode
ErrorBoundary>
);
}In the browser, that's a TRPCClientError:
- If the query failed before it was dehydrated, it isn't sent. The browser fetches it again and gets a
TRPCClientError. - If it was dehydrated while pending and failed later, TanStack Query passes a redacted error to the browser. A handle read then refetches the query once, even with
retry: false, so error boundaries get aTRPCClientErrorwithdata.code. A query that always fails costs one extra request.
To log or report these errors in one place, whichever component reads the handle, use the onError callback of your QueryClient's QueryCache.
Invalidation and staleness
Handle reads use normal tRPC query keys, so invalidation doesn't change. Pass any of these to queryClient.invalidateQueries:
{ queryKey: handle.queryKey }matches the handle's own query, plain or infinite.trpc.post.pathFilter()matches every query underpost, plain and infinite.trpc.post.byId.queryFilter(input)matches a query handle. An infinite handle's key hastype: 'infinite', soqueryFiltermisses it: usetrpc.post.comments.infiniteQueryFilter(input).
To update a handle's data in place, build its key from the procedure and handle.input: trpc.post.byId.queryKey(handle.input), or infiniteQueryKey for an infinite handle. setQueryData then checks the data's type. handle.queryKey is a plain array, so that the handle serializes, and carries no data type.
const queryClient: QueryClientqueryClient.QueryClient.setQueryData<unknown, TRPCQueryKeyWithoutPrefix & {
[dataTagSymbol]: Post;
[dataTagErrorSymbol]: TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>;
}, Post>(queryKey: TRPCQueryKeyWithoutPrefix & {
[dataTagSymbol]: Post;
[dataTagErrorSymbol]: TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>;
}, updater: Updater<...>, options?: SetDataOptions): NoInfer<Post> | undefined
setQueryData(const trpc: TRPCtrpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | null | undefined;
};
output: {
items: Post[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId.DecorateQueryProcedure<{ input: { id: string; }; output: Post; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.queryKey: (input?: Partial<{
id: string;
}> | undefined) => TRPCQueryKeyWithoutPrefix & {
[dataTagSymbol]: Post;
[dataTagErrorSymbol]: TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>;
}
queryKey(const handles: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}
handles.post: TRPCQueryHandle<{
input: {
id: string;
};
output: Post;
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post.TRPCQueryHandle<{ input: { id: string; }; output: Post; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.input: {
id: string;
}
input), (post: Post | undefinedpost) =>
post: Post | undefinedpost ? { ...post: Postpost, title: stringtitle: 'Renamed' } : post: undefinedpost,
);
const queryClient: QueryClientqueryClient.QueryClient.setQueryData<unknown, TRPCQueryKeyWithoutPrefix & {
[dataTagSymbol]: TRPCInfiniteData<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>;
[dataTagErrorSymbol]: TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>;
}, TRPCInfiniteData<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, {
...;
}>>(queryKey: TRPCQueryKeyWithoutPrefix & {
[dataTagSymbol]: TRPCInfiniteData<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>;
[dataTagErrorSymbol]: TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>;
}, updater: Updater<...>, options?: SetDataOptions): NoInfer<TRPCInfiniteData<...>> | undefined
setQueryData(
const trpc: TRPCtrpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: Post;
meta: object;
errorShape: DefaultErrorShape;
}>;
byAuthor: QueryProcedure<{
input: {
authorId: string;
cursor?: number | null | undefined;
};
output: {
items: Post[];
nextCursor: number | null;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
comments: QueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
addComment: MutationProcedure<...>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.comments: DecorateQueryProcedure<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & DecorateInfiniteQueryProcedure<...>
comments.DecorateInfiniteQueryProcedure<{ input: { postId: string; limit?: number | undefined; cursor?: number | null | undefined; }; output: { items: { id: string; text: string; }[]; nextCursor: number | null; }; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.infiniteQueryKey: (input?: Partial<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}> | undefined) => TRPCQueryKeyWithoutPrefix & {
[dataTagSymbol]: TRPCInfiniteData<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>;
[dataTagErrorSymbol]: TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>;
}
infiniteQueryKey(const handles: {
post: inferQueryHandle<TRPC["post"]["byId"]>;
comments: inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}
handles.comments: TRPCInfiniteQueryHandle<{
input: {
postId: string;
limit?: number | undefined;
cursor?: number | ... 1 more ... | undefined;
};
output: {
...;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
comments.TRPCInfiniteQueryHandle<{ input: { postId: string; limit?: number | undefined; cursor?: number | null | undefined; }; output: { items: { id: string; text: string; }[]; nextCursor: number | null; }; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.input: Omit<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, "cursor" | "direction">
input),
(comments: TRPCInfiniteData<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}> | undefined
comments) =>
comments: TRPCInfiniteData<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}> | undefined
comments && {
...comments: TRPCInfiniteData<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>
comments,
InfiniteData<{ items: { id: string; text: string; }[]; nextCursor: number | null; }, number | null>.pages: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}[]
pages: comments: TRPCInfiniteData<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>
comments.InfiniteData<{ items: { id: string; text: string; }[]; nextCursor: number | null; }, number | null>.pages: {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}[]
pages.Array<{ items: { id: string; text: string; }[]; nextCursor: number | null; }>.slice(start?: number, end?: number): {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}[]
slice(0, 1),
InfiniteData<{ items: { id: string; text: string; }[]; nextCursor: number | null; }, number | null>.pageParams: (number | null)[]pageParams: comments: TRPCInfiniteData<{
postId: string;
limit?: number | undefined;
cursor?: number | null | undefined;
}, {
items: {
id: string;
text: string;
}[];
nextCursor: number | null;
}>
comments.InfiniteData<{ items: { id: string; text: string; }[]; nextCursor: number | null; }, number | null>.pageParams: (number | null)[]pageParams.Array<number | null>.slice(start?: number, end?: number): (number | null)[]slice(0, 1),
},
);fromHandle keeps TanStack Query's refetch defaults. Hydrated data keeps the server's fetch time, so set a staleTime above 0 in your query client, as the setup guides do, or the client refetches right after hydration. To change it for one read, pass staleTime to fromHandle.
What a handle guarantees
- Types: only
loadHandlescreates handles. The key, procedure, input and kind (query or infinite) always agree, and output,selectand error types flow to the read. - Runtime:
fromHandlechecks the handle's shape andkeyPrefix.loadHandlesrejects an entry built with a proxy for anotherQueryClient. - Development: if a handle's query runs during server rendering, its data never reached that
QueryClient. tRPC logs a warning once per query. The warning is off in production.
Handles aren't signed. A handle names a query the browser could call anyway, and every refetch runs input validation and auth on the server again. Put permission checks in your procedures.
Testing components that take handles
Handles can't be written by hand, so create them in the test with a proxy over a test QueryClient. Seed the cache with fixture data, then load the handle with a prefetch entry, which leaves fresh data alone. The reader finds the fixture without calling the procedure:
import { class QueryClientQueryClient } from '@tanstack/react-query';
import { function createTRPCOptionsProxy<TRouter extends AnyRouter, TFeatureFlags extends FeatureFlags = {
keyPrefix: false;
}>(opts: TRPCOptionsProxyOptions<TRouter, TFeatureFlags>): TRPCOptionsProxy<TRouter, TFeatureFlags>
createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import { function loadHandles<TFeatureFlags extends FeatureFlags, TEntries extends Record<string, AnyTRPCEntry>>(trpc: DecorateRouterKeyable<TFeatureFlags>, entries: TEntries): Promise<TRPCLoadedHandles<TEntries>>loadHandles, function prefetchEntry<TDef extends ResolverDef>(procedure: DecorateQueryProcedure<TDef>, ...args: EntryArgs<TDef>): TRPCQueryEntry<TDef, "prefetch">prefetchEntry } from '@trpc/tanstack-react-query/unstable-handles';
import { const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
appRouter } from './router';
const const queryClient: QueryClientqueryClient = new new QueryClient(config?: QueryClientConfig): QueryClientQueryClient({
QueryClientConfig.defaultOptions?: DefaultOptions<Error> | undefineddefaultOptions: { DefaultOptions<Error>.queries?: OmitKeyof<QueryObserverOptions<unknown, Error, unknown, unknown, readonly unknown[], never>, "suspense" | "queryKey", "strictly"> | undefinedqueries: { staleTime?: StaleTimeFunction<unknown, Error, unknown, readonly unknown[]> | undefinedstaleTime: var Infinity: numberInfinity, retry?: RetryValue<Error> | undefinedretry: false } },
});
const const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
trpc = createTRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>(opts: TRPCOptionsProxyOptions<...>): TRPCOptionsProxy<...>
createTRPCOptionsProxy({ TRPCOptionsProxyOptionsInternal<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ post: BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<...>>; }>>>.router: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
router: const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
appRouter, TRPCOptionsProxyOptionsInternal<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ post: BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<...>>; }>>>.ctx: object | (() => MaybePromise<object>)ctx: {}, queryClient: QueryClient | (() => QueryClient)queryClient });
const queryClient: QueryClientqueryClient.QueryClient.setQueryData<unknown, TRPCQueryKeyWithoutPrefix & {
[dataTagSymbol]: {
id: string;
title: string;
};
[dataTagErrorSymbol]: TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>;
}, {
id: string;
title: string;
}>(queryKey: TRPCQueryKeyWithoutPrefix & {
[dataTagSymbol]: {
id: string;
title: string;
};
[dataTagErrorSymbol]: TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>;
}, updater: Updater<...>, options?: SetDataOptions): NoInfer<{
id: string;
title: string;
}> | undefined
setQueryData(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId.DecorateQueryProcedure<{ input: { id: string; }; output: { id: string; title: string; }; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.queryKey: (input?: Partial<{
id: string;
}> | undefined) => TRPCQueryKeyWithoutPrefix & {
[dataTagSymbol]: {
id: string;
title: string;
};
[dataTagErrorSymbol]: TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>;
}
queryKey({ id?: string | undefinedid: '1' }), {
id: stringid: '1',
title: stringtitle: 'Hello',
});
const [{ const post: TRPCQueryHandle<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>
post }] = await loadHandles<FeatureFlags, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}>(trpc: DecorateRouterKeyable<FeatureFlags>, entries: {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">;
}): Promise<...>
loadHandles(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
trpc, {
post: TRPCQueryEntry<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
post: prefetchEntry<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>(procedure: DecorateQueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}>, input: {
id: string;
}, opts?: TRPCEntryOptions | undefined): TRPCQueryEntry<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}, "prefetch">
prefetchEntry(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, {
...;
}>
trpc.post: DecoratedRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>, {
...;
}> & DecorateRouterKeyable<...>
post.byId: DecorateQueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
byId, { id: stringid: '1' }),
});- Render the component under a
QueryClientProviderandTRPCProviderwith the samequeryClient. The component reads the fixture from the cache. - Leave out
setQueryDatato run the real procedure, or use a client withlocalLinkinstead ofrouterto test through your links. - For a loading state, leave out
setQueryDataand use a procedure that never resolves: the reader suspends.
Where handles don't work
- Server functions and server actions: they run with a
QueryClientthat never hydrates the page. Return data from them, not handles. - URLs and storage: a handle in the URL or
localStoragegains nothing over the input itself.
FAQ
Why functions instead of methods on trpc?
The methods on trpc, such as queryOptions, queryKey and pathFilter, return plain TanStack Query options, keys or filters. Handles are tRPC helpers built on top of them, so they're separate functions from @trpc/tanstack-react-query/unstable-handles that take a procedure: ensureEntry(trpc.post.byId, { id }). Apps that don't use handles don't load their code. Each entry is one TanStack Query call: see TanStack Query equivalents.
Why one load call instead of a call per query?
So a loader can't create a waterfall by the order of its lines. With a call per query, awaiting the post before starting the comments makes the comments wait for the post, and nothing warns you. A load starts every entry before it waits for any, whatever order you list them in. It also gives you the typed data of the entries it waited for, so a loader can check it without reading the cache. The cost is one load call and one entry, even for a loader that loads a single query.