@trpc/tanstack-react-query
Version:
TanStack React Query Integration for tRPC
900 lines (838 loc) • 28.1 kB
text/typescript
import type {
FetchQueryOptions,
GetNextPageParamFunction,
QueryClient,
QueryFunction,
} from '@tanstack/react-query';
import { hashKey, isCancelledError } from '@tanstack/react-query';
import { isTRPCClientError } from '@trpc/client';
import type {
DecorateInfiniteQueryProcedure,
DecorateQueryProcedure,
DecorateRouterKeyable,
} from '../internals/createOptionsProxy';
import type {
UnusedSkipTokenTRPCInfiniteQueryOptionsIn,
UnusedSkipTokenTRPCInfiniteQueryOptionsOut,
} from '../internals/infiniteQueryOptions';
import type {
UnusedSkipTokenTRPCQueryOptionsIn,
UnusedSkipTokenTRPCQueryOptionsOut,
} from '../internals/queryOptions';
import type {
ExtractCursorType,
FeatureFlags,
ResolverDef,
TRPCInfiniteData,
TRPCQueryBaseOptions,
TRPCQueryKey,
} from '../internals/types';
import { readQueryKey } from '../internals/utils';
import type { ErrorOf } from './entryError';
import { entryCause, TRPCEntryError } from './entryError';
declare const handleDef: unique symbol;
declare const entryDef: unique symbol;
type InfiniteHandleInput<TInput> = TInput extends object
? Omit<TInput, 'cursor' | 'direction'>
: TInput;
/**
* A plain tuple typed with the procedure's input. TanStack Router's serializability check
* rejects `unknown` inputs and `DataTag`-ged keys, so handles in `beforeLoad` need this shape.
*/
type HandleQueryKey<
TDef extends ResolverDef,
TInput,
TType extends 'query' | 'infinite',
> = TDef['featureFlags']['keyPrefix'] extends true
? [prefix: string[], path: string[], opts?: { input?: TInput; type?: TType }]
: [path: string[], opts?: { input?: TInput; type?: TType }];
/**
* A serializable reference to a query, from `loadHandles()`.
* Pass it to a component and read it with `fromHandle(trpc, handle)`.
*/
export interface TRPCQueryHandle<TDef extends ResolverDef = ResolverDef> {
/**
* Type-only: never present at runtime. Required, so only
* `loadHandles` creates handles.
*/
readonly [handleDef]: TDef & { kind: 'query' };
readonly queryKey: HandleQueryKey<TDef, TDef['input'], 'query'>;
/** The query's input. The key is absent when the input is `undefined`. */
readonly input: TDef['input'];
}
/**
* A serializable reference to an infinite query, from `loadHandles()`.
* Read it with `fromHandle(trpc, handle, { getNextPageParam })`.
*/
export interface TRPCInfiniteQueryHandle<
TDef extends ResolverDef = ResolverDef,
> {
/**
* Type-only: never present at runtime. Required, so only
* `loadHandles` creates handles.
*/
readonly [handleDef]: TDef & { kind: 'infinite' };
readonly queryKey: HandleQueryKey<
TDef,
InfiniteHandleInput<TDef['input']>,
'infinite'
>;
/** The query's input, without `cursor` and `direction` */
readonly input: InfiniteHandleInput<TDef['input']>;
readonly initialCursor?: NonNullable<ExtractCursorType<TDef['input']>> | null;
}
/** Any query or infinite query handle. */
export type AnyTRPCHandle = TRPCQueryHandle<any> | TRPCInfiniteQueryHandle<any>;
/**
* How `loadHandles` starts an entry's query, named after the
* `QueryClient` method it calls: `prefetchQuery`, `fetchQuery` or `ensureQueryData`.
*/
export type TRPCEntryKind = 'prefetch' | 'fetch' | 'ensure';
/**
* A query for `loadHandles()` to start, from `prefetchEntry`, `fetchEntry` or
* `ensureEntry`.
* Creating it starts nothing.
*/
export interface TRPCQueryEntry<
TDef extends ResolverDef = ResolverDef,
TKind extends TRPCEntryKind = TRPCEntryKind,
> {
/** Type-only: never present at runtime. */
readonly [entryDef]: { def: TDef; kind: TKind; type: 'query' };
}
/**
* An infinite query for `loadHandles()` to start, from `prefetchInfiniteEntry`,
* `fetchInfiniteEntry` or `ensureInfiniteEntry`. Creating it starts nothing.
*/
export interface TRPCInfiniteQueryEntry<
TDef extends ResolverDef = ResolverDef,
TKind extends TRPCEntryKind = TRPCEntryKind,
> {
/** Type-only: never present at runtime. */
readonly [entryDef]: { def: TDef; kind: TKind; type: 'infinite' };
}
/** Any query or infinite query entry. */
export type AnyTRPCEntry =
| TRPCQueryEntry<any, any>
| TRPCInfiniteQueryEntry<any, any>;
type HandleOfEntry<TEntry> =
TEntry extends TRPCQueryEntry<infer TDef, any>
? TRPCQueryHandle<TDef>
: TEntry extends TRPCInfiniteQueryEntry<infer TDef, any>
? TRPCInfiniteQueryHandle<TDef>
: never;
type DataOfEntry<TEntry> =
TEntry extends TRPCQueryEntry<infer TDef, any>
? TDef['output']
: TEntry extends TRPCInfiniteQueryEntry<infer TDef, any>
? TRPCInfiniteData<TDef['input'], TDef['output']>
: never;
type HandlesOf<TEntries extends Record<string, AnyTRPCEntry>> = {
[TKey in keyof TEntries]: HandleOfEntry<TEntries[TKey]>;
};
type DefOfEntry<TEntry> = TEntry extends {
readonly [entryDef]: { def: infer TDef extends ResolverDef };
}
? TDef
: never;
/** Only fetch and ensure entries are waited for, so only they have data */
type WaitedKey<TEntries, TKey extends keyof TEntries> = TEntries[TKey] extends {
readonly [entryDef]: { kind: 'fetch' | 'ensure' };
}
? TKey
: never;
type DataOf<TEntries extends Record<string, AnyTRPCEntry>> = {
[TKey in keyof TEntries as WaitedKey<TEntries, TKey>]: DataOfEntry<
TEntries[TKey]
>;
};
type ResultsOf<TEntries extends Record<string, AnyTRPCEntry>> = {
[TKey in keyof TEntries as WaitedKey<TEntries, TKey>]: TRPCEntryResult<
DefOfEntry<TEntries[TKey]>,
DataOfEntry<TEntries[TKey]>
>;
};
/**
* What `loadHandlesSettled()` has for a fetch or ensure entry: its data, or
* the error it failed with.
* @see https://trpc.io/docs/client/tanstack-react-query/handles#errors-as-values
*/
export type TRPCEntryResult<TDef extends ResolverDef, TData> =
| { readonly status: 'success'; readonly data: TData }
| { readonly status: 'error'; readonly error: TRPCEntryError<TDef> };
/**
* What `loadHandles()` resolves to: `[handles, data]`. `handles` has a handle
* for every entry, by the entry's key; return them from the loader. `data` has the data of
* every fetch and ensure entry, by the entry's key.
* @see https://trpc.io/docs/client/tanstack-react-query/handles#loader-data
*/
export type TRPCLoadedHandles<TEntries extends Record<string, AnyTRPCEntry>> =
readonly [handles: HandlesOf<TEntries>, data: DataOf<TEntries>];
/**
* What `loadHandlesSettled()` resolves to: `[handles, results]`. `results`
* has a `TRPCEntryResult` for every fetch and ensure entry, by the entry's key.
* @see https://trpc.io/docs/client/tanstack-react-query/handles#errors-as-values
*/
export type TRPCSettledHandles<TEntries extends Record<string, AnyTRPCEntry>> =
readonly [handles: HandlesOf<TEntries>, results: ResultsOf<TEntries>];
/** Options for an entry */
export interface TRPCEntryOptions
extends
Pick<
FetchQueryOptions,
'staleTime' | 'retry' | 'retryDelay' | 'gcTime' | 'meta'
>,
TRPCQueryBaseOptions {}
type HandleGetNextPageParam<TDef extends ResolverDef> =
GetNextPageParamFunction<
NonNullable<ExtractCursorType<TDef['input']>> | null,
TDef['output']
>;
/**
* Options for an infinite query entry. Pass `pages` and `getNextPageParam`
* to load several pages up front.
* @see https://trpc.io/docs/client/tanstack-react-query/handles#infinite-queries
*/
export type TRPCInfiniteEntryOptions<TDef extends ResolverDef> =
TRPCEntryOptions & {
initialCursor?: NonNullable<ExtractCursorType<TDef['input']>> | null;
} & (
| {
pages?: never;
/**
* Without it, refetching a cached query with more than one page
* fails on the second page.
*/
getNextPageParam?: HandleGetNextPageParam<TDef>;
}
| {
/** Number of pages to load up front */
pages: number;
getNextPageParam: HandleGetNextPageParam<TDef>;
}
);
type EntryArgs<TDef extends ResolverDef> = undefined extends TDef['input']
? [input?: TDef['input'], opts?: TRPCEntryOptions]
: [input: TDef['input'], opts?: TRPCEntryOptions];
/**
* The handle type of a query procedure, for typing props.
* @example
* ```ts
* type PostHandle = inferQueryHandle<TRPC['post']['byId']>;
* ```
* @see https://trpc.io/docs/client/tanstack-react-query/handles#typing-props
*/
export type inferQueryHandle<TProcedure extends DecorateQueryProcedure<any>> =
TProcedure extends DecorateQueryProcedure<infer TDef>
? TRPCQueryHandle<TDef>
: never;
/**
* The handle type of an infinite query procedure, for typing props.
* @see https://trpc.io/docs/client/tanstack-react-query/handles#typing-props
*/
export type inferInfiniteQueryHandle<
TProcedure extends DecorateInfiniteQueryProcedure<any>,
> =
TProcedure extends DecorateInfiniteQueryProcedure<infer TDef>
? TRPCInfiniteQueryHandle<TDef>
: never;
const notAHandle = 'Expected a tRPC handle from `loadHandles()`';
function readHandle(handle: AnyTRPCHandle, prefix: string | undefined) {
const queryKey = handle?.queryKey as unknown as TRPCQueryKey<any> | undefined;
if (!Array.isArray(queryKey)) {
throw new Error(`${notAHandle}, got ${JSON.stringify(handle)}`);
}
const data = readQueryKey(queryKey);
const type = data.args?.type;
if (type !== 'query' && type !== 'infinite') {
throw new Error(
`${notAHandle}, got a key of type ${String(type)} for "${data.path.join('.')}"`,
);
}
if ((data.prefix?.[0] ?? undefined) !== prefix) {
throw new Error(
`The handle for "${data.path.join('.')}" was created with keyPrefix ${JSON.stringify(data.prefix?.[0])}, but this proxy uses ${JSON.stringify(prefix)}`,
);
}
return {
type,
path: data.path,
input: data.args?.input,
queryKey,
};
}
type HandleType = 'query' | 'infinite';
function toHandle(options: any, type: HandleType) {
const input = readQueryKey(options.queryKey).args?.input;
// Next.js rejects `undefined` in `getServerSideProps` / `getStaticProps` props
const handle: Record<string, unknown> = {
queryKey: options.queryKey,
...(input !== undefined && { input }),
};
if (type === 'infinite' && options.initialPageParam != null) {
handle['initialCursor'] = options.initialPageParam;
}
return handle;
}
const entryState = Symbol('trpc.handleEntry');
interface EntryState {
kind: TRPCEntryKind;
type: HandleType;
options: any;
/** The `QueryClient` of the proxy that built the entry */
queryClient: QueryClient;
}
function createEntry(state: EntryState) {
return { [entryState]: state };
}
function readEntry(key: string, entry: unknown): EntryState {
const state = (entry as { [entryState]?: EntryState } | null)?.[entryState];
if (!state) {
throw new Error(
`Expected an entry from \`prefetchEntry()\`, \`fetchEntry()\` or \`ensureEntry()\` for "${key}", got ${JSON.stringify(entry)}`,
);
}
return state;
}
function startEntry(queryClient: QueryClient, state: EntryState) {
const { options } = state;
if (state.type === 'infinite') {
switch (state.kind) {
case 'prefetch':
return queryClient.prefetchInfiniteQuery(options);
case 'ensure':
return queryClient.ensureInfiniteQueryData({
...options,
revalidateIfStale: true,
});
case 'fetch':
return queryClient.fetchInfiniteQuery(options);
}
}
switch (state.kind) {
case 'prefetch':
return queryClient.prefetchQuery(options);
case 'ensure':
return queryClient.ensureQueryData({
...options,
revalidateIfStale: true,
});
case 'fetch':
return queryClient.fetchQuery(options);
}
}
function toEntryError(key: string, state: EntryState, error: unknown) {
if (isCancelledError(error)) {
return error;
}
return new TRPCEntryError({
key,
path: readQueryKey(state.options.queryKey).path.join('.'),
...entryCause(error),
});
}
function settleLoad(promise: Promise<unknown>) {
return promise.then(
(data) => ({ status: 'success', data }),
(error: unknown) => {
if (error instanceof TRPCEntryError) {
return { status: 'error', error };
}
throw error;
},
);
}
async function startEntries(opts: {
queryClient: QueryClient;
entries: Record<string, unknown>;
/** Resolve each waited entry to a `TRPCEntryResult` instead of rejecting */
settle: boolean;
}) {
const { queryClient, entries } = opts;
const states = Object.entries(entries).map(([key, entry]) => {
const state = readEntry(key, entry);
if (state.queryClient !== queryClient) {
throw new Error(
`The entry for "${key}" was built with a proxy for another QueryClient than the root proxy that loads it`,
);
}
return [key, state] as const;
});
const loads = states.map(([key, state]) => ({
key,
state,
promise: startEntry(queryClient, state).catch((error: unknown) => {
throw toEntryError(key, state, error);
}),
}));
const waited = loads.filter((load) => load.state.kind !== 'prefetch');
const results = await Promise.all(
waited.map((load) =>
opts.settle ? settleLoad(load.promise) : load.promise,
),
);
const handles = Object.fromEntries(
loads.map((load) => [
load.key,
toHandle(load.state.options, load.state.type),
]),
);
const data = Object.fromEntries(
waited.map((load, index) => [load.key, results[index]]),
);
return [handles, data] as const;
}
function isServerEnvironment() {
return typeof window === 'undefined' || 'Deno' in globalThis;
}
const warnedNotHydrated = new Set<string>();
function warnIfNotHydrated(
path: readonly string[],
queryFn: QueryFunction<any, any, any>,
): QueryFunction<any, any, any> {
return (context) => {
if (isServerEnvironment()) {
const queryHash = hashKey(context.queryKey);
if (!warnedNotHydrated.has(queryHash)) {
warnedNotHydrated.add(queryHash);
// oxlint-disable-next-line no-console
console.warn(
`[@trpc/tanstack-react-query] The handle for "${path.join('.')}" is being fetched again during server rendering. ` +
`Unless its data is stale, the query it refers to isn't in this QueryClient. ` +
`Render the reading component inside <HydrateClient> or <HydrationBoundary>, and don't return handles from server functions or server actions. ` +
`See https://trpc.io/docs/client/tanstack-react-query/handles#where-handles-dont-work`,
);
}
}
return queryFn(context);
};
}
const watchedHydrations = new WeakSet<Promise<unknown>>();
/**
* A query dehydrated as pending that later rejects reaches the browser as a
* redacted error, before any `retry` from `fromHandle()` applies. Refetch such
* a query once so error boundaries see a `TRPCClientError` from this client.
*/
function refetchRejectedHydration(
queryClient: QueryClient,
type: HandleType,
options: any,
) {
const enabled =
typeof options.enabled === 'function'
? options.enabled
: () => options.enabled;
if (isServerEnvironment()) {
return;
}
const query = queryClient
.getQueryCache()
.find({ queryKey: options.queryKey, exact: true });
const promise = query?.promise;
if (
!query ||
!promise ||
query.state.data !== undefined ||
query.state.fetchStatus === 'idle' ||
enabled(query) === false ||
watchedHydrations.has(promise)
) {
return;
}
watchedHydrations.add(promise);
promise.catch((error: unknown) => {
if (
isTRPCClientError(error) ||
isCancelledError(error) ||
query.state.fetchStatus !== 'idle' ||
enabled(query) === false
) {
return;
}
const defaulted = queryClient.defaultQueryOptions(options);
// Older query-core versions take the infinite behavior from the fetch's
// options, so `query.fetch` would store a bare page there.
// `fetchInfiniteQuery` skips retries unless asked, unlike a query's fetch
// in the browser, which retries 3 times by default.
const refetch =
type === 'infinite'
? queryClient.fetchInfiniteQuery({
...options,
retry: defaulted.retry ?? 3,
})
: query.fetch(defaulted);
refetch.catch(() => {
// surfaces through the query's state
});
});
}
function prepareHandleRead<TOptions extends { queryFn?: unknown }>(opts: {
queryClient: QueryClient;
handle: ReturnType<typeof readHandle>;
options: TOptions;
}): TOptions {
const { options } = opts;
if (
globalThis.process?.env?.['NODE_ENV'] !== 'production' &&
typeof options.queryFn === 'function'
) {
options.queryFn = warnIfNotHydrated(
opts.handle.path,
options.queryFn as QueryFunction<any, any, any>,
);
}
refetchRejectedHydration(opts.queryClient, opts.handle.type, options);
return options;
}
interface ProxyContext {
proxy: any;
path: string[];
prefix: string | undefined;
}
function readProxy(proxy: unknown, fn: string): ProxyContext {
const queryKey = (proxy as { queryKey?: unknown } | null)?.queryKey;
if (Array.isArray(queryKey)) {
throw new Error(
`\`${fn}()\` expects a tRPC options proxy or one of its procedures, got a handle for \`${readQueryKey(queryKey as TRPCQueryKey<any>).path.join('.')}\``,
);
}
if (typeof queryKey !== 'function') {
throw new Error(
`\`${fn}()\` expects a tRPC options proxy or one of its procedures, got ${String(proxy)}`,
);
}
const key = readQueryKey(queryKey());
return {
proxy,
path: key.path,
prefix: key.prefix?.[0],
};
}
function queryClientOf(ctx: ProxyContext): QueryClient {
return ctx.proxy['~queryClient']();
}
function optionsOf(
procedure: any,
type: HandleType,
): (input: unknown, opts: unknown) => any {
return type === 'infinite'
? procedure.infiniteQueryOptions
: procedure.queryOptions;
}
function readRoot(trpc: unknown, fn: string) {
const ctx = readProxy(trpc, fn);
if (ctx.path.length > 0) {
throw new Error(
`\`${fn}()\` takes the root proxy, \`trpc\`, got \`trpc.${ctx.path.join('.')}\``,
);
}
return ctx;
}
function entry(opts: {
fn: string;
type: HandleType;
kind: TRPCEntryKind;
procedure: unknown;
input: unknown;
opts: unknown;
}) {
const ctx = readProxy(opts.procedure, opts.fn);
return createEntry({
type: opts.type,
kind: opts.kind,
options: optionsOf(ctx.proxy, opts.type)(opts.input, opts.opts),
queryClient: queryClientOf(ctx),
});
}
/**
* Whether `error` is a failed entry from `loadHandles()`. Pass a procedure to
* check that the entry was for it, which types `error.cause` with its errors.
* @example
* ```ts
* const [handles] = await loadHandles(trpc, {
* post: ensureEntry(trpc.post.byId, { id }),
* }).catch((error: unknown) => {
* if (isTRPCEntryError(error, trpc.post.byId) && error.code === 'NOT_FOUND') {
* throw notFound();
* }
* throw error;
* });
* ```
* @see https://trpc.io/docs/client/tanstack-react-query/handles#errors-ensure
*/
export function isTRPCEntryError<TDef extends ResolverDef>(
error: unknown,
procedure: DecorateQueryProcedure<TDef>,
): error is TRPCEntryError<TDef>;
export function isTRPCEntryError(error: unknown): error is TRPCEntryError;
export function isTRPCEntryError(error: unknown, procedure?: unknown) {
if (!(error instanceof TRPCEntryError)) {
return false;
}
return (
procedure === undefined ||
error.path === readProxy(procedure, 'isTRPCEntryError').path.join('.')
);
}
/**
* Starts every entry before it waits for any, so the order of the entries doesn't
* matter. Resolves to `[handles, data]` once every fetch and ensure entry has data:
* a handle for every entry, and the data of the fetch and ensure entries. Rejects as
* soon as a fetch or ensure entry fails. Prefetch entries never make it reject.
*
* Takes the root of an options proxy, from `createTRPCOptionsProxy` or `useTRPC()`.
* Call it in a route loader, a server component or an event handler, not during
* render: every render would start the entries again.
* @example
* ```ts
* const [handles, data] = await loadHandles(trpc, {
* comments: prefetchInfiniteEntry(trpc.post.comments, { postId: id }),
* post: ensureEntry(trpc.post.byId, { id }),
* });
* if (!data.post.published) throw notFound();
* return handles;
* ```
* @see https://trpc.io/docs/client/tanstack-react-query/handles
*/
export function loadHandles<
TFeatureFlags extends FeatureFlags,
TEntries extends Record<string, AnyTRPCEntry>,
>(
trpc: DecorateRouterKeyable<TFeatureFlags>,
entries: TEntries,
): Promise<TRPCLoadedHandles<TEntries>> {
return startEntries({
queryClient: queryClientOf(readRoot(trpc, 'loadHandles')),
entries,
settle: false,
}) as Promise<any>;
}
/**
* Like `loadHandles()`, but it waits for every fetch and ensure entry to
* settle and never rejects because one failed. Resolves to `[handles, results]`:
* `results` has `{ status: 'success', data }` or `{ status: 'error', error }`,
* with a `TRPCEntryError`, for every fetch and ensure entry.
*
* A cancelled query still rejects, as it does with `loadHandles()`.
* @example
* ```ts
* const [handles, results] = await loadHandlesSettled(trpc, {
* post: ensureEntry(trpc.post.byId, { id }),
* comments: prefetchInfiniteEntry(trpc.post.comments, { postId: id }),
* });
* if (results.post.status === 'error') {
* if (results.post.error.code === 'NOT_FOUND') throw notFound();
* throw results.post.error;
* }
* ```
* @see https://trpc.io/docs/client/tanstack-react-query/handles#errors-as-values
*/
export function loadHandlesSettled<
TFeatureFlags extends FeatureFlags,
TEntries extends Record<string, AnyTRPCEntry>,
>(
trpc: DecorateRouterKeyable<TFeatureFlags>,
entries: TEntries,
): Promise<TRPCSettledHandles<TEntries>> {
return startEntries({
queryClient: queryClientOf(readRoot(trpc, 'loadHandlesSettled')),
entries,
settle: true,
}) as Promise<any>;
}
/**
* An entry for `loadHandles()` that starts the query without waiting, like
* `queryClient.prefetchQuery`. It never makes the load reject: errors surface where
* the handle is read. Creating the entry starts nothing.
* @see https://trpc.io/docs/client/tanstack-react-query/handles#prefetch
*/
export function prefetchEntry<TDef extends ResolverDef>(
procedure: DecorateQueryProcedure<TDef>,
...args: EntryArgs<TDef>
): TRPCQueryEntry<TDef, 'prefetch'>;
export function prefetchEntry(
procedure: unknown,
input?: unknown,
opts?: unknown,
): unknown {
return entry({
fn: 'prefetchEntry',
type: 'query',
kind: 'prefetch',
procedure,
input,
opts,
});
}
/**
* An entry for `loadHandles()` that waits for fresh data, like `queryClient.fetchQuery`:
* cached data is used only if it isn't stale. The load rejects if the request fails.
* Creating the entry starts nothing.
* @see https://trpc.io/docs/client/tanstack-react-query/handles#fetch
*/
export function fetchEntry<TDef extends ResolverDef>(
procedure: DecorateQueryProcedure<TDef>,
...args: EntryArgs<TDef>
): TRPCQueryEntry<TDef, 'fetch'>;
export function fetchEntry(
procedure: unknown,
input?: unknown,
opts?: unknown,
): unknown {
return entry({
fn: 'fetchEntry',
type: 'query',
kind: 'fetch',
procedure,
input,
opts,
});
}
/**
* An entry for `loadHandles()` that uses any cached data, even stale, like
* `queryClient.ensureQueryData({ revalidateIfStale: true })`. It fetches and waits only
* when the cache has no data, and the load rejects if that request fails. Stale data is
* refetched in the background. Creating the entry starts nothing.
* @see https://trpc.io/docs/client/tanstack-react-query/handles#ensure
*/
export function ensureEntry<TDef extends ResolverDef>(
procedure: DecorateQueryProcedure<TDef>,
...args: EntryArgs<TDef>
): TRPCQueryEntry<TDef, 'ensure'>;
export function ensureEntry(
procedure: unknown,
input?: unknown,
opts?: unknown,
): unknown {
return entry({
fn: 'ensureEntry',
type: 'query',
kind: 'ensure',
procedure,
input,
opts,
});
}
/**
* Like `prefetchEntry`, for an infinite query. Loads the first page unless `pages` is set.
* @see https://trpc.io/docs/client/tanstack-react-query/handles#infinite-queries
*/
export function prefetchInfiniteEntry<TDef extends ResolverDef>(
procedure: DecorateInfiniteQueryProcedure<TDef>,
input: TDef['input'],
opts?: TRPCInfiniteEntryOptions<TDef>,
): TRPCInfiniteQueryEntry<TDef, 'prefetch'> {
return entry({
fn: 'prefetchInfiniteEntry',
type: 'infinite',
kind: 'prefetch',
procedure,
input,
opts,
}) as any;
}
/**
* Like `fetchEntry`, for an infinite query.
* @see https://trpc.io/docs/client/tanstack-react-query/handles#infinite-queries
*/
export function fetchInfiniteEntry<TDef extends ResolverDef>(
procedure: DecorateInfiniteQueryProcedure<TDef>,
input: TDef['input'],
opts?: TRPCInfiniteEntryOptions<TDef>,
): TRPCInfiniteQueryEntry<TDef, 'fetch'> {
return entry({
fn: 'fetchInfiniteEntry',
type: 'infinite',
kind: 'fetch',
procedure,
input,
opts,
}) as any;
}
/**
* Like `ensureEntry`, for an infinite query.
* @see https://trpc.io/docs/client/tanstack-react-query/handles#infinite-queries
*/
export function ensureInfiniteEntry<TDef extends ResolverDef>(
procedure: DecorateInfiniteQueryProcedure<TDef>,
input: TDef['input'],
opts?: TRPCInfiniteEntryOptions<TDef>,
): TRPCInfiniteQueryEntry<TDef, 'ensure'> {
return entry({
fn: 'ensureInfiniteEntry',
type: 'infinite',
kind: 'ensure',
procedure,
input,
opts,
}) as any;
}
/**
* Options for a handle: the same options `trpc.<path>.queryOptions(input)` or
* `trpc.<path>.infiniteQueryOptions(input, opts)` return for the handle's procedure and
* input. Pass them to `useQuery`, `useSuspenseQuery`, `useSuspenseInfiniteQuery`, etc.
* `trpc` is the proxy or a router on the handle's path, usually `useTRPC()`.
* @see https://trpc.io/docs/client/tanstack-react-query/handles#reading-a-handle
*/
export 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']
>;
export function fromHandle<TDef extends ResolverDef, TData = TDef['output']>(
trpc: DecorateRouterKeyable<NoInfer<TDef['featureFlags']>>,
handle: TRPCInfiniteQueryHandle<TDef>,
opts: UnusedSkipTokenTRPCInfiniteQueryOptionsIn<
TDef['input'],
NoInfer<TDef['output']>,
TData,
ErrorOf<TDef>,
TDef['featureFlags']
>,
): UnusedSkipTokenTRPCInfiniteQueryOptionsOut<
TDef['input'],
TDef['output'],
TData,
ErrorOf<TDef>,
TDef['featureFlags']
>;
export function fromHandle(
trpc: unknown,
rawHandle: AnyTRPCHandle,
opts?: object,
): unknown {
const ctx = readProxy(trpc, 'fromHandle');
const handle = readHandle(rawHandle, ctx.prefix);
if (!ctx.path.every((segment, index) => handle.path[index] === segment)) {
throw new Error(
`\`fromHandle(trpc.${ctx.path.join('.')}, handle)\` got a handle for \`${handle.path.join('.')}\`, which isn't in \`${ctx.path.join('.')}\`. Pass the root proxy, \`trpc\``,
);
}
const procedure = handle.path
.slice(ctx.path.length)
.reduce((level, segment) => level[segment], ctx.proxy);
const options = optionsOf(procedure, handle.type)(
handle.input,
handle.type === 'infinite'
? {
initialCursor: (rawHandle as TRPCInfiniteQueryHandle).initialCursor,
...opts,
}
: opts,
);
return prepareHandleRead({
queryClient: queryClientOf(ctx),
handle,
options,
});
}