import { DehydratedState, QueryClient, CreateInfiniteQueryOptions, CreateInfiniteQueryResult, CreateMutationOptions, CreateMutationResult, CreateQueryOptions, CreateQueryResult, createInfiniteQuery as __useInfiniteQuery, createMutation as __useMutation, createQuery as __useQuery, hashQueryKey, useQueryClient, QueryClientProviderProps, QueryClientProvider, } from "@tanstack/solid-query"; import { CreateTRPCClientOptions, TRPCClient, TRPCClientErrorLike, TRPCRequestOptions, createTRPCClient, } from "@trpc/client"; import type { AnyRouter, ProcedureRecord, inferHandlerInput, inferProcedureClientError, inferProcedureInput, inferProcedureOutput, inferSubscriptionOutput, } from "@trpc/server"; import { inferObservableValue } from "@trpc/server/observable"; import { Accessor, Context, createEffect, createMemo, createSignal, JSX, onCleanup, onMount, useContext as __useContext, } from "solid-js"; import { SSRState, TRPCContext, TRPCContextProps, TRPCContextState, } from "../../internals/context"; import { getArrayQueryKey } from "../../internals/getArrayQueryKey"; import { CreateTRPCSolidOptions, UseMutationOverride } from "../types"; export type OutputWithCursor = { cursor: TCursor | null; data: TData; }; export interface TRPCReactRequestOptions // For RQ, we use their internal AbortSignals instead of letting the user pass their own extends Omit { /** * Opt out of SSR for this query by passing `ssr: false` */ ssr?: boolean; /** * Opt out or into aborting request on unmount */ abortOnUnmount?: boolean; } export interface TRPCUseQueryBaseOptions { /** * tRPC-related options */ trpc?: TRPCReactRequestOptions; } export type { TRPCContext, TRPCContextState } from "../../internals/context"; export interface UseTRPCQueryOptions extends CreateQueryOptions [TPath, TInput]>, TRPCUseQueryBaseOptions {} export interface UseTRPCInfiniteQueryOptions extends CreateInfiniteQueryOptions< TOutput, TError, TOutput, TOutput, () => [TPath, TInput] >, TRPCUseQueryBaseOptions {} export interface UseTRPCMutationOptions< TInput, TError, TOutput, TContext = unknown > extends CreateMutationOptions, TRPCUseQueryBaseOptions {} export interface UseTRPCSubscriptionOptions { enabled?: boolean; onStarted?: () => void; onData: (data: TOutput) => void; onError?: (err: TError) => void; } function getClientArgs( pathAndInput: TPathAndInput, opts: TOptions ) { const [path, input] = pathAndInput; return [path, input, (opts as any)?.trpc] as const; } type inferInfiniteQueryNames = { [TPath in keyof TObj]: inferProcedureInput extends { cursor?: any; } ? TPath : never; }[keyof TObj]; type inferProcedures = { [TPath in keyof TObj]: { input: inferProcedureInput; output: inferProcedureOutput; }; }; export interface TRPCProviderProps extends TRPCContextProps { children: JSX.Element; } export type TRPCProvider = ( props: TRPCProviderProps & { queryClientOpts?: Omit; } ) => JSX.Element; export type UseDehydratedState = ( client: TRPCClient, trpcState: DehydratedState | undefined ) => Accessor; export type CreateClient = ( opts: CreateTRPCClientOptions ) => TRPCClient; interface TRPCHookResult { trpc: { path: string; }; } /** * @internal */ export type UseTRPCQueryResult = CreateQueryResult< TData, TError > & TRPCHookResult; /** * @internal */ export type UseTRPCInfiniteQueryResult = CreateInfiniteQueryResult & TRPCHookResult; /** * @internal */ export type UseTRPCMutationResult = CreateMutationResult & TRPCHookResult; /** * Makes a stable reference of the `trpc` prop */ function useHookResult(value: TRPCHookResult["trpc"]): TRPCHookResult["trpc"] { const ref = { current: value }; ref.current.path = value.path; return ref.current; } /** * Create strongly typed react hooks * @internal */ export function createHooksInternal< TRouter extends AnyRouter, TSSRContext = unknown >(config?: CreateTRPCSolidOptions) { const mutationSuccessOverride: UseMutationOverride["onSuccess"] = config?.unstable_overrides?.useMutation?.onSuccess ?? ((options) => options.originalFn()); type TQueries = TRouter["_def"]["queries"]; type TSubscriptions = TRouter["_def"]["subscriptions"]; type TMutations = TRouter["_def"]["mutations"]; type TError = TRPCClientErrorLike; type TInfiniteQueryNames = inferInfiniteQueryNames; type TQueryValues = inferProcedures; type TMutationValues = inferProcedures; type ProviderContext = Omit< TRPCContextState, "ssrState" > & { ssrState: Accessor["ssrState"]>; }; const Context = (config?.context ?? TRPCContext) as Context; const SolidQueryContext = config?.solidQueryContext as Context< QueryClient | undefined >; const createClient: CreateClient = (opts) => { return createTRPCClient(opts); }; const TRPCProvider: TRPCProvider = (props) => { const { abortOnUnmount = false, client, queryClient, ssrContext } = props; const [ssrState, setSSRState] = createSignal( props.ssrState ?? false ); onMount(() => { // Only updating state to `mounted` if we are using SSR. // This makes it so we don't have an unnecessary re-render when opting out of SSR. setSSRState((state) => (state ? "mounted" : false)); }); return ( { return queryClient.fetchQuery( getArrayQueryKey(pathAndInput), () => (client as any).query(...getClientArgs(pathAndInput, opts)), opts ); }, fetchInfiniteQuery: (pathAndInput, opts) => { return queryClient.fetchInfiniteQuery( getArrayQueryKey(pathAndInput), ({ pageParam }) => { const [path, input] = pathAndInput; const actualInput = { ...(input as any), cursor: pageParam }; return (client as any).query( ...getClientArgs([path, actualInput], opts) ); }, opts ); }, prefetchQuery: (pathAndInput, opts) => { return queryClient.prefetchQuery( getArrayQueryKey(pathAndInput), () => (client as any).query(...getClientArgs(pathAndInput, opts)), opts ); }, prefetchInfiniteQuery: (pathAndInput, opts) => { return queryClient.prefetchInfiniteQuery( getArrayQueryKey(pathAndInput), ({ pageParam }) => { const [path, input] = pathAndInput; const actualInput = { ...(input as any), cursor: pageParam }; return (client as any).query( ...getClientArgs([path, actualInput], opts) ); }, opts ); }, invalidateQueries: (...args: any[]) => { const [queryKey, ...rest] = args; return queryClient.invalidateQueries( getArrayQueryKey(queryKey), ...rest ); }, refetchQueries: (...args: any[]) => { const [queryKey, ...rest] = args; return queryClient.refetchQueries( getArrayQueryKey(queryKey), ...rest ); }, cancelQuery: (pathAndInput) => { return queryClient.cancelQueries(getArrayQueryKey(pathAndInput)); }, setQueryData: (...args) => { const [queryKey, ...rest] = args; return queryClient.setQueryData( getArrayQueryKey(queryKey), ...rest ); }, getQueryData: (...args) => { const [queryKey, ...rest] = args; return queryClient.getQueryData( getArrayQueryKey(queryKey), ...rest ); }, setInfiniteQueryData: (...args) => { const [queryKey, ...rest] = args; return queryClient.setQueryData( getArrayQueryKey(queryKey), ...rest ); }, getInfiniteQueryData: (...args) => { const [queryKey, ...rest] = args; return queryClient.getQueryData( getArrayQueryKey(queryKey), ...rest ); }, }} > {props.children} ); }; function useContext() { return __useContext(Context); } /** * Hack to make sure errors return `status`='error` when doing SSR * @link https://github.com/trpc/trpc/pull/1645 */ function useSSRQueryOptionsIfNeeded< TOptions extends { retryOnMount?: boolean } | undefined >(pathAndInput: unknown[], opts: TOptions): TOptions { const { queryClient, ssrState } = useContext(); return ssrState() && ssrState() !== "mounted" && queryClient.getQueryCache().find(getArrayQueryKey(pathAndInput))?.state .status === "error" ? { retryOnMount: false, ...opts, } : opts; } function useQuery< TPath extends keyof TQueryValues & string, TQueryFnData = TQueryValues[TPath]["output"], TData = TQueryValues[TPath]["output"] >( pathAndInput: [path: TPath, ...args: inferHandlerInput], opts?: UseTRPCQueryOptions< TPath, TQueryValues[TPath]["input"], TQueryFnData, TData, TError > ): UseTRPCQueryResult { const ctx = useContext(); if ( typeof window === "undefined" && ctx.ssrState() === "prepass" && opts?.trpc?.ssr !== false && opts?.enabled !== false && !ctx.queryClient.getQueryCache().find(getArrayQueryKey(pathAndInput)) ) { void ctx.prefetchQuery(pathAndInput as any, opts as any); } const ssrOpts = useSSRQueryOptionsIfNeeded(pathAndInput, opts); // request option should take priority over global const shouldAbortOnUnmount = opts?.trpc?.abortOnUnmount ?? ctx?.abortOnUnmount ?? false; const hook = __useQuery( () => getArrayQueryKey(pathAndInput), (queryFunctionContext) => { const actualOpts = { ...ssrOpts, trpc: { ...ssrOpts?.trpc, ...(shouldAbortOnUnmount ? { signal: queryFunctionContext.signal } : {}), }, }; return (ctx.client as any).query( ...getClientArgs(pathAndInput, actualOpts) ); }, { context: SolidQueryContext, ...ssrOpts } as any ) as UseTRPCQueryResult; hook.trpc = useHookResult({ path: pathAndInput[0], }); return hook; } function useMutation< TPath extends keyof TMutationValues & string, TContext = unknown >( path: TPath | [TPath], opts?: UseTRPCMutationOptions< TMutationValues[TPath]["input"], TError, TMutationValues[TPath]["output"], TContext > ): UseTRPCMutationResult< TMutationValues[TPath]["output"], TError, TMutationValues[TPath]["input"], TContext > { const ctx = useContext(); const queryClient = useQueryClient(); const hook = __useMutation( (input) => { const actualPath = Array.isArray(path) ? path[0] : path; return (ctx.client.mutation as any)( ...getClientArgs([actualPath, input], opts) ); }, { context: SolidQueryContext, ...opts, onSuccess(...args) { const originalFn = () => opts?.onSuccess?.(...args); return mutationSuccessOverride({ originalFn, queryClient }); }, } ) as UseTRPCMutationResult< TMutationValues[TPath]["output"], TError, TMutationValues[TPath]["input"], TContext >; hook.trpc = useHookResult({ path: Array.isArray(path) ? path[0] : path, }); return hook; } /* istanbul ignore next */ /** * ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ * **Experimental.** API might change without major version bump * ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠ */ function useSubscription< TPath extends keyof TSubscriptions & string, TOutput extends inferSubscriptionOutput >( pathAndInput: [ path: TPath, ...args: inferHandlerInput ], opts: UseTRPCSubscriptionOptions< inferObservableValue>, inferProcedureClientError > ) { const enabled = opts?.enabled ?? true; const ctx = useContext(); return createEffect(() => { if (!enabled) { return; } // noop (() => { return hashQueryKey(pathAndInput); })(); const [path, input] = pathAndInput; let isStopped = false; const subscription = ctx.client.subscription< TRouter["_def"]["subscriptions"], TPath, TOutput, inferProcedureInput >(path, (input ?? undefined) as any, { onStarted: () => { if (!isStopped) { opts.onStarted?.(); } }, onData: (data) => { if (!isStopped) { opts.onData(data); } }, onError: (err) => { if (!isStopped) { opts.onError?.(err); } }, }); onCleanup(() => { isStopped = true; subscription.unsubscribe(); }); }); } function useInfiniteQuery( pathAndInput: [ path: TPath, input: Omit ], opts?: UseTRPCInfiniteQueryOptions< TPath, Omit, TQueryValues[TPath]["output"], TError > ): UseTRPCInfiniteQueryResult { const [path, input] = pathAndInput; const ctx = useContext(); if ( typeof window === "undefined" && ctx.ssrState() === "prepass" && opts?.trpc?.ssr !== false && opts?.enabled !== false && !ctx.queryClient.getQueryCache().find(getArrayQueryKey(pathAndInput)) ) { void ctx.prefetchInfiniteQuery(pathAndInput as any, opts as any); } const ssrOpts = useSSRQueryOptionsIfNeeded(pathAndInput, opts); // request option should take priority over global const shouldAbortOnUnmount = opts?.trpc?.abortOnUnmount ?? ctx?.abortOnUnmount ?? false; const hook = __useInfiniteQuery( () => getArrayQueryKey(pathAndInput), (queryFunctionContext) => { const actualOpts = { ...ssrOpts, trpc: { ...ssrOpts?.trpc, ...(shouldAbortOnUnmount ? { signal: queryFunctionContext.signal } : {}), }, }; const actualInput = { ...((input as any) ?? {}), cursor: queryFunctionContext.pageParam, }; return (ctx.client as any).query( ...getClientArgs([path, actualInput], actualOpts) ); }, { context: SolidQueryContext, ...ssrOpts } as any ) as UseTRPCInfiniteQueryResult; hook.trpc = useHookResult({ path, }); return hook; } const useDehydratedState: UseDehydratedState = ( client, trpcState ) => { const transformed: Accessor = createMemo( () => { if (!trpcState) { return trpcState; } return client.runtime.transformer.deserialize(trpcState); } ); return transformed; }; return { Provider: TRPCProvider, createClient, useContext, useQuery, useMutation, useSubscription, useDehydratedState, useInfiniteQuery, }; } /** * Hack to infer the type of `createReactQueryHooks` * @link https://stackoverflow.com/a/59072991 */ class GnClass { fn() { return createHooksInternal(); } } type returnTypeInferer = TType extends ( a: Record ) => infer U ? U : never; type fooType = GnClass< TRouter, TSSRContext >["fn"]; /** * Infer the type of a `createSolidQueryHooks` function * @internal */ export type CreateSolidQueryHooks< TRouter extends AnyRouter, TSSRContext = unknown > = returnTypeInferer>;