import { CreateTRPCClientOptions, TRPCClient, TRPCClientErrorLike, TRPCRequestOptions, createTRPCClient, } from '@trpc/client'; import type { AnyRouter, ProcedureRecord, inferHandlerInput, inferProcedureInput, inferProcedureOutput, inferSubscriptionOutput, } from '@trpc/server'; import React, { ReactNode, useCallback, useEffect, useMemo, useState, } from 'react'; import { DehydratedState, QueryClient, UseInfiniteQueryOptions, UseInfiniteQueryResult, UseMutationOptions, UseMutationResult, UseQueryOptions, UseQueryResult, useInfiniteQuery as __useInfiniteQuery, useMutation as __useMutation, useQuery as __useQuery, hashQueryKey, } from 'react-query'; import { SSRState, TRPCContext, TRPCContextState } from './internals/context'; export type OutputWithCursor = { cursor: TCursor | null; data: TData; }; export interface TRPCUseQueryBaseOptions extends TRPCRequestOptions { /** * Opt out of SSR for this query by passing `ssr: false` */ ssr?: boolean; } export interface UseTRPCQueryOptions extends UseQueryOptions, TRPCUseQueryBaseOptions {} export interface UseTRPCInfiniteQueryOptions extends UseInfiniteQueryOptions< TOutput, TError, TOutput, TOutput, [TPath, TInput] >, TRPCUseQueryBaseOptions {} export interface UseTRPCMutationOptions< TInput, TError, TOutput, TContext = unknown, > extends UseMutationOptions, TRPCUseQueryBaseOptions {} function getClientArgs( pathAndInput: TPathAndInput, opts: TOptions, ) { const [path, input] = pathAndInput; return [path, input, opts] as const; } type inferInfiniteQueryNames< TObj extends ProcedureRecord, > = { [TPath in keyof TObj]: inferProcedureInput extends { cursor?: any; } ? TPath : never; }[keyof TObj]; type inferProcedures< TObj extends ProcedureRecord, > = { [TPath in keyof TObj]: { input: inferProcedureInput; output: inferProcedureOutput; }; }; export function createReactQueryHooks< TRouter extends AnyRouter, TSSRContext = unknown, >() { type TQueries = TRouter['_def']['queries']; type TSubscriptions = TRouter['_def']['subscriptions']; type TError = TRPCClientErrorLike; type TInfiniteQueryNames = inferInfiniteQueryNames; type TQueryValues = inferProcedures; type TMutationValues = inferProcedures; type ProviderContext = TRPCContextState; const Context = TRPCContext as React.Context; function createClient( opts: CreateTRPCClientOptions, ): TRPCClient { return createTRPCClient(opts); } function TRPCProvider(props: { queryClient: QueryClient; client: TRPCClient; children: ReactNode; /** * @deprecated */ isPrepass?: boolean; ssrContext?: TSSRContext | null; ssrState?: SSRState; }) { const { client, queryClient, ssrContext } = props; const [ssrState, setSSRState] = useState( props.ssrState || (props.isPrepass ? 'prepass' : false), ); useEffect(() => { // 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( pathAndInput, () => (client as any).query(...getClientArgs(pathAndInput, opts)), opts, ); }, [client, queryClient], ), fetchInfiniteQuery: useCallback( (pathAndInput, opts) => { return queryClient.fetchInfiniteQuery( pathAndInput, ({ pageParam }) => { const [path, input] = pathAndInput; const actualInput = { ...(input as any), cursor: pageParam }; return (client as any).query( ...getClientArgs([path, actualInput], opts), ); }, opts, ); }, [client, queryClient], ), prefetchQuery: useCallback( (pathAndInput, opts) => { return queryClient.prefetchQuery( pathAndInput, () => (client as any).query(...getClientArgs(pathAndInput, opts)), opts, ); }, [client, queryClient], ), prefetchInfiniteQuery: useCallback( (pathAndInput, opts) => { return queryClient.prefetchInfiniteQuery( pathAndInput, ({ pageParam }) => { const [path, input] = pathAndInput; const actualInput = { ...(input as any), cursor: pageParam }; return (client as any).query( ...getClientArgs([path, actualInput], opts), ); }, opts, ); }, [client, queryClient], ), /** * @deprecated use `invalidateQueries` */ invalidateQuery: useCallback( (...args: any[]) => queryClient.invalidateQueries(...args), [queryClient], ), invalidateQueries: useCallback( (...args: any[]) => queryClient.invalidateQueries(...args), [queryClient], ), refetchQueries: useCallback( (...args: any[]) => queryClient.refetchQueries(...args), [queryClient], ), cancelQuery: useCallback( (pathAndInput) => { return queryClient.cancelQueries(pathAndInput); }, [queryClient], ), setQueryData: useCallback( (...args) => queryClient.setQueryData(...args), [queryClient], ), getQueryData: useCallback( (...args) => queryClient.getQueryData(...args), [queryClient], ), setInfiniteQueryData: useCallback( (...args) => { return queryClient.setQueryData(...args); }, [queryClient], ), getInfiniteQueryData: useCallback( (...args) => queryClient.getQueryData(...args), [queryClient], ), }} > {props.children} ); } function useContext() { return React.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(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 >, ): UseQueryResult { const { client, isPrepass, queryClient, prefetchQuery } = useContext(); if ( typeof window === 'undefined' && isPrepass && opts?.ssr !== false && opts?.enabled !== false && !queryClient.getQueryCache().find(pathAndInput) ) { prefetchQuery(pathAndInput as any, opts as any); } const actualOpts = useSSRQueryOptionsIfNeeded(pathAndInput, opts); return __useQuery( pathAndInput as any, () => (client as any).query(...getClientArgs(pathAndInput, actualOpts)), actualOpts, ); } function useMutation< TPath extends keyof TMutationValues & string, TContext = unknown, >( path: TPath | [TPath], opts?: UseTRPCMutationOptions< TMutationValues[TPath]['input'], TError, TMutationValues[TPath]['output'], TContext >, ): UseMutationResult< TMutationValues[TPath]['output'], TError, TMutationValues[TPath]['input'], TContext > { const { client } = useContext(); return __useMutation((input) => { const actualPath = Array.isArray(path) ? path[0] : path; return (client.mutation as any)(actualPath, input, opts); }, opts); } /* 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: { enabled?: boolean; onError?: (err: TError) => void; onNext: (data: TOutput) => void; }, ) { const enabled = opts?.enabled ?? true; const queryKey = hashQueryKey(pathAndInput); const { client } = useContext(); return useEffect(() => { if (!enabled) { return; } const [path, input] = pathAndInput; let isStopped = false; const unsub = client.subscription(path, (input ?? undefined) as any, { onError: (err) => { if (!isStopped) { opts.onError?.(err); } }, onNext: (res) => { if (res.type === 'data' && !isStopped) { opts.onNext(res.data); } }, }); return () => { isStopped = true; unsub(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [queryKey, enabled]); } function useInfiniteQuery( pathAndInput: [ path: TPath, input: Omit, ], opts?: UseTRPCInfiniteQueryOptions< TPath, Omit, TQueryValues[TPath]['output'], TError >, ): UseInfiniteQueryResult { const [path, input] = pathAndInput; const { client, isPrepass, prefetchInfiniteQuery, queryClient } = useContext(); if ( typeof window === 'undefined' && isPrepass && opts?.ssr !== false && opts?.enabled !== false && !queryClient.getQueryCache().find(pathAndInput) ) { prefetchInfiniteQuery(pathAndInput as any, opts as any); } const actualOpts = useSSRQueryOptionsIfNeeded(pathAndInput, opts); return __useInfiniteQuery( pathAndInput as any, ({ pageParam }) => { const actualInput = { ...((input as any) ?? {}), cursor: pageParam }; return (client as any).query( ...getClientArgs([path, actualInput], actualOpts), ); }, actualOpts, ); } function useDehydratedState( client: TRPCClient, trpcState: DehydratedState | undefined, ) { const transformed: DehydratedState | undefined = useMemo(() => { if (!trpcState) { return trpcState; } return client.runtime.transformer.deserialize(trpcState); }, [client, trpcState]); return transformed; } return { Provider: TRPCProvider, createClient, useContext, useQuery, useMutation, useSubscription, useDehydratedState, useInfiniteQuery, }; }