Skip to content
tRPC

Partners

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;
Property 'comments' does not exist on type 'DataOf<{ post: TRPCQueryEntry<{ input: { id: string; }; output: Post; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }, "ensure">; comments: TRPCInfiniteQueryEntry<...>; }>'.

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;
post: TRPCQueryHandle<{
    input: {
        id: string;
    };
    output: Post;
    transformer: false;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>
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;
post: 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. Return handles from the loader, or pass them to components as props. Reading a prefetch entry's data is 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 useSuspenseQuery and no cached data, it suspends: wrap it in <Suspense> to show a fallback in its place. With useQuery, it renders with status: 'pending', and with server rendering it can cause a hydration mismatch: see Reading with useQuery.

    <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 handles: HandlesOf<{
        post: TRPCQueryEntry<{
            input: {
                id: string;
            };
            output: Post;
            transformer: false;
            errorShape: DefaultErrorShape;
            featureFlags: {
                keyPrefix: false;
            };
        }, "prefetch">;
    }>
    handles
    .
    post: TRPCQueryHandle<{
        input: {
            id: string;
        };
        output: Post;
        transformer: false;
        errorShape: DefaultErrorShape;
        featureFlags: {
            keyPrefix: false;
        };
    }>
    post
    } />
    </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: useQuery returns it as status: 'error', and useSuspenseQuery throws 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. Reading data.post is 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.post has 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 useSuspenseQuery or useQuery. 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, like queryClient.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's staleReloadMode, to 'blocking'.
  • In server components, the two behave the same: the request's QueryClient starts empty, so both wait for the request.

Which to use

prefetchEntryensureEntryfetchEntry
Page waits for dataNoYesYes
In dataNoYesYes
With stale dataRefetches in the backgroundResolves at once, refetches in the backgroundWaits for the refetch
Reader's first renderLoading state, or cached dataThe dataThe data
When the request failsThe reader gets the errorThe load rejectsThe load rejects
Start queries in a loaderOpen all posts or another article, then come back. Within 5 seconds, the page uses the cache. After that, the data is stale. Each request takes its own time, a little different on every run.
Start queries with
Latency

The page shows at once, with skeletons until the data comes in. Stale data shows at once and refetches.

← All posts

Comments

Network

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:

tRPCTanStack 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, fetchInfiniteEntryprefetchInfiniteQuery, 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

  • input is optional when the procedure's input is optional, so prefetchEntry(trpc.viewer) works.
  • opts takes staleTime, gcTime, retry, retryDelay, meta, and trpc request options such as context. They apply to the load's request. Readers use the options you pass to fromHandle.

Where to load handles

Your appLoad handles inWith
Next.js App RouterServer componentsThe server proxy from trpc/server.tsx
TanStack Start, TanStack RouterRoute loadersThe proxy in your router context
React Routerloader or clientLoaderSee React Router
No loaders or server components, such as a SPAEvent handlers, such as a link's hoveruseTRPC()
  • In server components, the request's QueryClient starts empty, so an ensure entry always waits for the request.
  • loadHandles and the entries take any tRPC options proxy: one from createTRPCOptionsProxy, or useTRPC(). Type a router context with TRPCOptionsProxy<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 loadHandles during 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, with useSuspenseQuery(trpc.x.queryOptions(input)).
  • loadHandles takes 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.
Keep the sort in
Page 1 of 5
  • 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

  1. loaderpost.list({ sort: 'new', page: 1 })done
The URL holds the whole input. Every navigation, the back button and a reload give the loader everything it needs, so the list starts before the page renders.

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:

server/routers/_app.ts
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)),
}), });
The loader for /posts/$slug
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:

server/routers/_app.ts
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,
}); }), }), });
The loader for /posts/$slug
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
)],
});
  • opts takes everything queryOptions takes, such as select, staleTime, refetchInterval and trpc.
  • fromHandle takes any level of the proxy: fromHandle(trpc, handle) and fromHandle(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.queryKey is the cache entry's key. For a typed queryClient.getQueryData, pass fromHandle(trpc, handle).queryKey. The handle's own key isn't tagged with its data type, so the handle stays serializable everywhere, including TanStack Router's beforeLoad.
  • handle.input is the typed input the query was started with, so a component doesn't need it as a separate prop. Infinite handles leave out cursor and direction.

Typing props

Export your proxy type once, then type props with inferQueryHandle:

trpc/client.tsx
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
>;
app/posts/[id]/post.tsx
'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:

app/posts/[id]/post.tsx
'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>;
} }
  • useQuery doesn't suspend. Until the data arrives, it renders with status: '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: with useQuery, 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 on useQuery, 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:

  • useSuspenseQuery types data as defined. The component suspends until the cache has the data.
  • useQuery types data as possibly undefined, as for any query. Switch on status to 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. A useSuspenseQuery reader suspends and shows the nearest Suspense fallback. A useQuery reader renders with status: 'pending'.
  • queryClient.removeQueries() and queryClient.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();
post: Post
  • data has no key for prefetch entries, since their query may have no data yet.
  • The data is the same object the cache holds. Return handles, not data, 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;
InfiniteData<{ items: { id: string; text: string; }[]; nextCursor: number | null; }, number | null>.pages: {
    items: {
        id: string;
        text: string;
    }[];
    nextCursor: number | null;
}[]

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:

comments.tsx
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, infiniteQueryOptions returns the same query key, so the first render reads the loader's cache entry instead of fetching again. For a query handle, use useQuery(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 setSort in startTransition to 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:

Where the queries startReload, open all posts, or open another article, and watch when each request starts and ends. Each one takes its own time.
Start the queries in
Latency

The route loader starts every query from the URL: the article's id, or the list's page. One round trip.

← All posts

Comments

Network

No requests yet.

To keep it that way:

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 MoreByAuthor renders, 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, beforeLoad can 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 in beforeLoad.

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:

app/trpc.ts
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 });
app/routes/post.tsx
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:

app/router.tsx
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 QueryClient the 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 Suspense boundaries 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:

app/trpc/server.ts
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
};
}
app/root.tsx
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> ); }
app/routes/post.tsx
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> ); }
  • makeQueryClient is the query client factory from the server components guide: it dehydrates pending queries and runs your transformer's serialize and deserialize.
  • 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 with dehydrate's shouldDehydrateQuery if 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's QueryClient starts 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:

trpc/server.tsx
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:

app/posts/[id]/page.tsx
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 HydrateClient sent in the same request isn't sent again, so nesting them is fine. The outer one renders before CommentsSection starts 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 dehydrate options: shouldDehydrateQuery, serializeData and the rest.
  • It works only in server components. @trpc/tanstack-react-query resolves to its server build through the react-server export 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:

app/posts/[id]/page.tsx
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 Suspense boundary, so one slow query doesn't hold back the rest.
  • If the post doesn't exist, the Post component throws, and the nearest error.tsx shows 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():

app/posts/[id]/page.tsx
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 Post renders, so it shows the post at once. If the cache loses the post later, Post suspends and shows the nearest loading.tsx or Suspense fallback: 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 a TRPCError on the server proxy, and a TRPCClientError on 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
);
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;
    };
}>
} 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;
data: Post
  • It waits for every fetch and ensure entry, even after one has failed, so you see all their failures, not just the first.
  • results has no key for prefetch entries, as data hasn't with loadHandles.
  • handles has 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 a TRPCClientError with data.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 under post, plain and infinite.
  • trpc.post.byId.queryFilter(input) matches a query handle. An infinite handle's key has type: 'infinite', so queryFilter misses it: use trpc.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 loadHandles creates handles. The key, procedure, input and kind (query or infinite) always agree, and output, select and error types flow to the read.
  • Runtime: fromHandle checks the handle's shape and keyPrefix. loadHandles rejects an entry built with a proxy for another QueryClient.
  • 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:

post.test.tsx
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 QueryClientProvider and TRPCProvider with the same queryClient. The component reads the fixture from the cache.
  • Leave out setQueryData to run the real procedure, or use a client with localLink instead of router to test through your links.
  • For a loading state, leave out setQueryData and use a procedure that never resolves: the reader suspends.

Where handles don't work

  • Server functions and server actions: they run with a QueryClient that never hydrates the page. Return data from them, not handles.
  • URLs and storage: a handle in the URL or localStorage gains 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.