@trpc/tanstack-react-query
Version:
TanStack React Query Integration for tRPC
679 lines (641 loc) • 20.1 kB
text/typescript
import type { DataTag, QueryClient, QueryFilters } from '@tanstack/react-query';
import type {
TRPCClient,
TRPCClientErrorLike,
TRPCRequestOptions,
} from '@trpc/client';
import { getUntypedClient, TRPCUntypedClient } from '@trpc/client';
import type {
AnyTRPCProcedure,
AnyTRPCRootTypes,
AnyTRPCRouter,
inferProcedureErrorShape,
inferProcedureInput,
inferRouterContext,
inferTransformedProcedureOutput,
TRPCProcedureType,
TRPCRouterRecord,
} from '@trpc/server';
import { callTRPCProcedure, createTRPCRecursiveProxy } from '@trpc/server';
import type { MaybePromise } from '@trpc/server/unstable-core-do-not-import';
import type {
DecorateHandleReaders,
DecorateInfiniteQueryHandleVerbs,
DecorateQueryHandleVerbs,
HandleVerb,
} from './handles';
import { prepareHandleRead, readHandle, startHandle } from './handles';
import {
trpcInfiniteQueryOptions,
type TRPCInfiniteQueryOptions,
} from './infiniteQueryOptions';
import type { MutationOptionsOverride } from './mutationOptions';
import {
trpcMutationOptions,
type TRPCMutationOptions,
} from './mutationOptions';
import { trpcQueryOptions, type TRPCQueryOptions } from './queryOptions';
import {
trpcSubscriptionOptions,
type TRPCSubscriptionOptions,
} from './subscriptionOptions';
import type {
DefaultFeatureFlags,
FeatureFlags,
KeyPrefixOptions,
OptionalCursorInput,
ResolverDef,
TRPCInfiniteData,
TRPCMutationKey,
TRPCQueryKey,
WithRequired,
} from './types';
import {
getMutationKeyInternal,
getQueryKeyInternal,
unwrapLazyArg,
} from './utils';
/**
* Key and filter helpers for a router path.
*/
export interface DecorateRouterKeyable<TFeatureFlags extends FeatureFlags> {
/**
* Calculate the TanStack Query Key for any path, could be used to invalidate every procedure beneath this path
*
* @see https://tanstack.com/query/latest/docs/framework/react/guides/query-keys
* @see https://trpc.io/docs/client/tanstack-react-query/usage#queryKey
*/
pathKey: () => TRPCQueryKey<TFeatureFlags['keyPrefix']>;
/**
* Calculate a TanStack Query Filter for any path, could be used to manipulate every procedure beneath this path
*
* @see https://tanstack.com/query/latest/docs/framework/react/guides/filters
* @see https://trpc.io/docs/client/tanstack-react-query/usage#queryFilter
*/
pathFilter: (
filters?: QueryFilters<TRPCQueryKey<TFeatureFlags['keyPrefix']>>,
) => WithRequired<
QueryFilters<TRPCQueryKey<TFeatureFlags['keyPrefix']>>,
'queryKey'
>;
}
interface TypeHelper<TDef extends ResolverDef> {
/**
* @internal prefer using inferInput and inferOutput to access types
*/
'~types': {
input: TDef['input'];
output: TDef['output'];
errorShape: TDef['errorShape'];
};
}
/**
* The input type of a procedure on the options proxy.
* @see https://trpc.io/docs/client/tanstack-react-query/usage#inferring-input-and-output-types
*/
export type inferInput<
TProcedure extends
| DecorateInfiniteQueryProcedure<any>
| DecorateQueryProcedure<any>
| DecorateMutationProcedure<any>,
> = TProcedure['~types']['input'];
/**
* The output type of a procedure on the options proxy.
* @see https://trpc.io/docs/client/tanstack-react-query/usage#inferring-input-and-output-types
*/
export type inferOutput<
TProcedure extends
| DecorateInfiniteQueryProcedure<any>
| DecorateQueryProcedure<any>
| DecorateMutationProcedure<any>,
> = TProcedure['~types']['output'];
/**
* The helpers for a query procedure whose input has a `cursor`.
* @see https://trpc.io/docs/client/tanstack-react-query/usage#infiniteQueryOptions
*/
export interface DecorateInfiniteQueryProcedure<TDef extends ResolverDef>
extends TypeHelper<TDef>, DecorateInfiniteQueryHandleVerbs<TDef> {
/**
* Create a set of type-safe infinite query options that can be passed to `useInfiniteQuery`, `prefetchInfiniteQuery` etc.
*
* @see https://tanstack.com/query/latest/docs/framework/react/reference/infiniteQueryOptions#infinitequeryoptions
* @see https://trpc.io/docs/client/tanstack-react-query/usage#infiniteQueryOptions
*/
infiniteQueryOptions: TRPCInfiniteQueryOptions<TDef>;
/**
* Calculate the TanStack Query Key for a Infinite Query Procedure
*
* @see https://tanstack.com/query/latest/docs/framework/react/guides/query-keys
* @see https://trpc.io/docs/client/tanstack-react-query/usage#queryKey
*/
infiniteQueryKey: (input?: Partial<TDef['input']>) => DataTag<
TRPCQueryKey<TDef['featureFlags']['keyPrefix']>,
TRPCInfiniteData<TDef['input'], TDef['output']>,
TRPCClientErrorLike<{
transformer: TDef['transformer'];
errorShape: TDef['errorShape'];
}>
>;
/**
* Calculate a TanStack Query Filter for a Infinite Query Procedure
*
* @see https://tanstack.com/query/latest/docs/framework/react/guides/filters
* @see https://trpc.io/docs/client/tanstack-react-query/usage#queryFilter
*/
infiniteQueryFilter: (
input?: Partial<TDef['input']>,
filters?: QueryFilters<
DataTag<
TRPCQueryKey<TDef['featureFlags']['keyPrefix']>,
TRPCInfiniteData<TDef['input'], TDef['output']>,
TRPCClientErrorLike<{
transformer: TDef['transformer'];
errorShape: TDef['errorShape'];
}>
>
>,
) => WithRequired<
QueryFilters<
DataTag<
TRPCQueryKey<TDef['featureFlags']['keyPrefix']>,
TRPCInfiniteData<TDef['input'], TDef['output']>,
TRPCClientErrorLike<{
transformer: TDef['transformer'];
errorShape: TDef['errorShape'];
}>
>
>,
'queryKey'
>;
}
/**
* The helpers for a query procedure.
* @see https://trpc.io/docs/client/tanstack-react-query/usage#queryOptions
*/
export interface DecorateQueryProcedure<TDef extends ResolverDef>
extends
TypeHelper<TDef>,
DecorateRouterKeyable<TDef['featureFlags']>,
DecorateQueryHandleVerbs<TDef> {
/**
* Create a set of type-safe query options that can be passed to `useQuery`, `prefetchQuery` etc.
*
* @see https://tanstack.com/query/latest/docs/framework/react/reference/queryOptions#queryoptions
* @see https://trpc.io/docs/client/tanstack-react-query/usage#queryOptions
*/
queryOptions: TRPCQueryOptions<TDef>;
/**
* Calculate the TanStack Query Key for a Query Procedure
*
* @see https://tanstack.com/query/latest/docs/framework/react/guides/query-keys
* @see https://trpc.io/docs/client/tanstack-react-query/usage#queryKey
*/
queryKey: (input?: Partial<TDef['input']>) => DataTag<
TRPCQueryKey<TDef['featureFlags']['keyPrefix']>,
TDef['output'],
TRPCClientErrorLike<{
transformer: TDef['transformer'];
errorShape: TDef['errorShape'];
}>
>;
/**
* Calculate a TanStack Query Filter for a Query Procedure
*
* @see https://tanstack.com/query/latest/docs/framework/react/guides/filters
* @see https://trpc.io/docs/client/tanstack-react-query/usage#queryFilter
*/
queryFilter: (
input?: Partial<TDef['input']>,
filters?: QueryFilters<
DataTag<
TRPCQueryKey<TDef['featureFlags']['keyPrefix']>,
TDef['output'],
TRPCClientErrorLike<{
transformer: TDef['transformer'];
errorShape: TDef['errorShape'];
}>
>
>,
) => WithRequired<
QueryFilters<
DataTag<
TRPCQueryKey<TDef['featureFlags']['keyPrefix']>,
TDef['output'],
TRPCClientErrorLike<{
transformer: TDef['transformer'];
errorShape: TDef['errorShape'];
}>
>
>,
'queryKey'
>;
}
/**
* The helpers for a mutation procedure.
* @see https://trpc.io/docs/client/tanstack-react-query/usage#mutationOptions
*/
export interface DecorateMutationProcedure<
TDef extends ResolverDef,
> extends TypeHelper<TDef> {
/**
* Create a set of type-safe mutation options that can be passed to `useMutation`
*
* @see https://trpc.io/docs/client/tanstack-react-query/usage#mutationOptions
*/
mutationOptions: TRPCMutationOptions<TDef>;
/**
* Calculate the TanStack Mutation Key for a Mutation Procedure
*
* @see https://trpc.io/docs/client/tanstack-react-query/usage#mutationKey
*/
mutationKey: () => TRPCMutationKey<TDef['featureFlags']['keyPrefix']>;
}
/**
* The helpers for a subscription procedure.
* @see https://trpc.io/docs/client/tanstack-react-query/usage#subscriptionOptions
*/
export interface DecorateSubscriptionProcedure<
TDef extends ResolverDef,
> extends TypeHelper<TDef> {
/**
* Create a set of type-safe subscription options that can be passed to `useSubscription`
*
* @see https://trpc.io/docs/client/tanstack-react-query/usage#subscriptionOptions
*/
subscriptionOptions: TRPCSubscriptionOptions<TDef>;
}
/**
* The helpers for a procedure of the given type.
*/
export type DecorateProcedure<
TType extends TRPCProcedureType,
TDef extends ResolverDef,
> = TType extends 'query'
? DecorateQueryProcedure<TDef> &
(TDef['input'] extends OptionalCursorInput
? DecorateInfiniteQueryProcedure<TDef>
: Record<string, never>)
: TType extends 'mutation'
? DecorateMutationProcedure<TDef>
: TType extends 'subscription'
? DecorateSubscriptionProcedure<TDef>
: never;
/**
* @internal
*/
export type DecoratedRouterRecord<
TRoot extends AnyTRPCRootTypes,
TRecord extends TRPCRouterRecord,
TFeatureFlags extends FeatureFlags = DefaultFeatureFlags,
> = {
[TKey in keyof TRecord]: TRecord[TKey] extends infer $Value
? $Value extends TRPCRouterRecord
? DecoratedRouterRecord<TRoot, $Value, TFeatureFlags> &
DecorateRouterKeyable<TFeatureFlags> &
DecorateHandleReaders<TFeatureFlags>
: $Value extends AnyTRPCProcedure
? DecorateProcedure<
$Value['_def']['type'],
{
input: inferProcedureInput<$Value>;
output: inferTransformedProcedureOutput<TRoot, $Value>;
transformer: TRoot['transformer'];
errorShape: inferProcedureErrorShape<TRoot, $Value>;
featureFlags: TFeatureFlags;
}
>
: never
: never;
};
/**
* The object returned by `useTRPC()` and `createTRPCOptionsProxy`. It mirrors your router and has query, mutation and subscription option helpers on each procedure.
* @see https://trpc.io/docs/client/tanstack-react-query/usage
*/
export type TRPCOptionsProxy<
TRouter extends AnyTRPCRouter,
TFeatureFlags extends FeatureFlags = DefaultFeatureFlags,
> = DecoratedRouterRecord<
TRouter['_def']['_config']['$types'],
TRouter['_def']['record'],
TFeatureFlags
> &
DecorateRouterKeyable<TFeatureFlags> &
DecorateHandleReaders<TFeatureFlags>;
/**
* Options shared by every {@link createTRPCOptionsProxy} call.
*/
export type TRPCOptionsProxyOptionsBase<
TFeatureFlags extends FeatureFlags = DefaultFeatureFlags,
> = {
/**
* The `QueryClient`, or a function that returns it.
*/
queryClient: QueryClient | (() => QueryClient);
/**
* @internal
*/
overrides?: {
mutations?: MutationOptionsOverride;
};
} & KeyPrefixOptions<TFeatureFlags>;
/**
* Call procedures on a router directly, for example on the server.
*/
export interface TRPCOptionsProxyOptionsInternal<
TRouter extends AnyTRPCRouter,
> {
/**
* The router to call.
*/
router: TRouter;
/**
* The context for each call, or a function that returns it.
*/
ctx:
| inferRouterContext<TRouter>
| (() => MaybePromise<inferRouterContext<TRouter>>);
}
/**
* Call procedures through a tRPC client.
*/
export interface TRPCOptionsProxyOptionsExternal<
TRouter extends AnyTRPCRouter,
> {
/**
* The tRPC client to call procedures with.
*/
client: TRPCUntypedClient<TRouter> | TRPCClient<TRouter>;
}
/**
* Options for {@link createTRPCOptionsProxy}. Pass either `client` or `router` and `ctx`.
*/
export type TRPCOptionsProxyOptions<
TRouter extends AnyTRPCRouter,
TFeatureFlags extends FeatureFlags = DefaultFeatureFlags,
> = TRPCOptionsProxyOptionsBase<TFeatureFlags> &
(
| TRPCOptionsProxyOptionsInternal<TRouter>
| TRPCOptionsProxyOptionsExternal<TRouter>
);
/**
* Marks the `useTRPC()` proxy, whose handle verbs run during render
* @internal
*/
export const renderHandleVerbs = Symbol('renderHandleVerbs');
type UtilsMethods =
| keyof DecorateQueryProcedure<any>
| keyof DecorateHandleReaders<any>
| keyof DecorateInfiniteQueryProcedure<any>
| keyof DecorateMutationProcedure<any>
| keyof DecorateSubscriptionProcedure<any>;
/**
* Creates the options proxy without React context. Works on the server too.
*
* @example
* ```ts
* export const trpc = createTRPCOptionsProxy<AppRouter>({
* client: trpcClient,
* queryClient,
* });
* ```
*
* @see https://trpc.io/docs/client/tanstack-react-query/setup#3c-set-up-without-react-context
* @see https://trpc.io/docs/client/tanstack-react-query/server-components#5-create-a-trpc-caller-for-server-components
*/
export function createTRPCOptionsProxy<
TRouter extends AnyTRPCRouter,
TFeatureFlags extends FeatureFlags = DefaultFeatureFlags,
>(
opts: TRPCOptionsProxyOptions<TRouter, TFeatureFlags>,
): TRPCOptionsProxy<TRouter, TFeatureFlags> {
const prefix = opts.keyPrefix;
const callIt = (type: TRPCProcedureType): any => {
return (path: string, input: unknown, trpcOpts: TRPCRequestOptions) => {
if ('router' in opts) {
return Promise.resolve(unwrapLazyArg(opts.ctx)).then((ctx) =>
callTRPCProcedure({
router: opts.router,
path: path,
getRawInput: async () => input,
ctx: ctx,
type: type,
signal: undefined,
batchIndex: 0,
}),
);
}
const untypedClient =
opts.client instanceof TRPCUntypedClient
? opts.client
: getUntypedClient(opts.client);
return untypedClient[type](path, input, trpcOpts);
};
};
return createTRPCRecursiveProxy(({ args, path: _path }) => {
const path = [..._path];
const utilName = path.pop() as UtilsMethods;
const [arg1, arg2] = args as any[];
const startQueryHandle = (kind: 'query' | 'infinite', verb: HandleVerb) => {
const inRender =
(opts as { [renderHandleVerbs]?: boolean })[renderHandleVerbs] === true;
if (inRender && verb !== 'prefetch') {
throw new Error(
`\`${[...path, utilName].join('.')}()\` on \`useTRPC()\` waits for its query, which a render can't. ` +
`Start queries in components with \`unstable_$prefetch\` or \`unstable_$prefetchInfinite\`, ` +
`and wait where you read the handle, with \`useSuspenseQuery\`. ` +
`See https://trpc.io/docs/client/tanstack-react-query/handles#in-render`,
);
}
const options =
kind === 'infinite'
? trpcInfiniteQueryOptions({
input: arg1,
opts: arg2,
path,
queryClient: opts.queryClient,
queryKey: getQueryKeyInternal({
path,
input: arg1,
type: 'infinite',
prefix,
}),
query: callIt('query'),
})
: trpcQueryOptions({
input: arg1,
opts: arg2,
path,
queryClient: opts.queryClient,
queryKey: getQueryKeyInternal({
path,
input: arg1,
type: 'query',
prefix,
}),
query: callIt('query'),
});
return startHandle({
queryClient: unwrapLazyArg(opts.queryClient),
options,
kind,
verb,
inRender,
});
};
const contextMap: Record<UtilsMethods, () => unknown> = {
'~types': undefined as any,
pathKey: () => {
return getQueryKeyInternal({
path,
type: 'any',
prefix,
});
},
pathFilter: (): QueryFilters => {
return {
...arg1,
queryKey: getQueryKeyInternal({
path,
type: 'any',
prefix,
}),
};
},
queryOptions: () => {
return trpcQueryOptions({
input: arg1,
opts: arg2,
path,
queryClient: opts.queryClient,
queryKey: getQueryKeyInternal({
path,
input: arg1,
type: 'query',
prefix,
}),
query: callIt('query'),
});
},
queryKey: () => {
return getQueryKeyInternal({
path,
input: arg1,
type: 'query',
prefix,
});
},
queryFilter: (): QueryFilters => {
return {
...arg2,
queryKey: getQueryKeyInternal({
path,
input: arg1,
type: 'query',
prefix,
}),
};
},
infiniteQueryOptions: () => {
return trpcInfiniteQueryOptions({
input: arg1,
opts: arg2,
path,
queryClient: opts.queryClient,
queryKey: getQueryKeyInternal({
path,
input: arg1,
type: 'infinite',
prefix,
}),
query: callIt('query'),
});
},
infiniteQueryKey: () => {
return getQueryKeyInternal({
path,
input: arg1,
type: 'infinite',
prefix,
});
},
infiniteQueryFilter: (): QueryFilters => {
return {
...arg2,
queryKey: getQueryKeyInternal({
path,
input: arg1,
type: 'infinite',
prefix,
}),
};
},
unstable_$fromHandle: () => {
const handle = readHandle(arg1, prefix);
const options =
handle.type === 'infinite'
? trpcInfiniteQueryOptions({
input: handle.input,
opts: {
initialCursor: arg1.initialCursor,
...arg2,
},
path: handle.path,
queryClient: opts.queryClient,
queryKey: handle.queryKey,
query: callIt('query'),
})
: trpcQueryOptions({
input: handle.input,
opts: arg2,
path: handle.path,
queryClient: opts.queryClient,
queryKey: handle.queryKey,
query: callIt('query'),
});
return prepareHandleRead(
unwrapLazyArg(opts.queryClient),
handle,
options,
);
},
unstable_$prefetch: () => startQueryHandle('query', 'prefetch'),
unstable_$ensure: () => startQueryHandle('query', 'ensure'),
unstable_$fetch: () => startQueryHandle('query', 'fetch'),
unstable_$prefetchInfinite: () =>
startQueryHandle('infinite', 'prefetch'),
unstable_$ensureInfinite: () => startQueryHandle('infinite', 'ensure'),
unstable_$fetchInfinite: () => startQueryHandle('infinite', 'fetch'),
mutationOptions: () => {
return trpcMutationOptions({
opts: arg1,
path,
prefix,
queryClient: opts.queryClient,
mutate: callIt('mutation'),
overrides: opts.overrides?.mutations,
});
},
mutationKey: () => {
return getMutationKeyInternal({
path,
prefix,
});
},
subscriptionOptions: () => {
return trpcSubscriptionOptions({
opts: arg2,
path,
input: arg1,
queryKey: getQueryKeyInternal({
path,
input: arg1,
type: 'any',
prefix,
}),
subscribe: callIt('subscription'),
});
},
};
return contextMap[utilName]();
});
}