UNPKG

@trpc/react-query

Version:

The tRPC React library

523 lines (520 loc) • 20.8 kB
import { useSuspenseInfiniteQuery, usePrefetchInfiniteQuery, useInfiniteQuery, skipToken, hashKey, useMutation, useSuspenseQueries, useQueries, useSuspenseQuery, usePrefetchQuery, useQuery } from '@tanstack/react-query'; import { createTRPCClient, TRPCUntypedClient, getUntypedClient } from '@trpc/client'; import { isAsyncIterable } from '@trpc/server/unstable-core-do-not-import'; import * as React from 'react'; import { TRPCContext } from '../../internals/context.mjs'; import { getClientArgs } from '../../internals/getClientArgs.mjs'; import { getQueryKeyInternal, getMutationKeyInternal } from '../../internals/getQueryKey.mjs'; import { useHookResult, buildQueryFromAsyncIterable } from '../../internals/trpcResult.mjs'; import { createUtilityFunctions } from '../../utils/createUtilityFunctions.mjs'; import { createUseQueries } from '../proxy/useQueriesProxy.mjs'; // TODO: Look into fixing react-compiler support const trackResult = (result, onTrackResult)=>{ const trackedResult = new Proxy(result, { get (target, prop) { onTrackResult(prop); return target[prop]; } }); return trackedResult; }; /** * @internal */ function createRootHooks(config) { const mutationSuccessOverride = config?.overrides?.useMutation?.onSuccess ?? ((options)=>options.originalFn()); const Context = config?.context ?? TRPCContext; const createClient = createTRPCClient; const TRPCProvider = (props)=>{ const { abortOnUnmount = false, queryClient, ssrContext } = props; const [ssrState, setSSRState] = React.useState(props.ssrState ?? false); const client = props.client instanceof TRPCUntypedClient ? props.client : getUntypedClient(props.client); const fns = React.useMemo(()=>createUtilityFunctions({ client, queryClient }), [ client, queryClient ]); const contextValue = React.useMemo(()=>({ abortOnUnmount, queryClient, client, ssrContext: ssrContext ?? null, ssrState, ...fns }), [ abortOnUnmount, client, fns, queryClient, ssrContext, ssrState ]); React.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 /*#__PURE__*/ React.createElement(Context.Provider, { value: contextValue }, props.children); }; function useContext() { const context = React.useContext(Context); if (!context) { throw new Error('Unable to find tRPC Context. Did you forget to wrap your App inside `withTRPC` HoC?'); } return context; } /** * Hack to make sure errors return `status`='error` when doing SSR * @see https://github.com/trpc/trpc/pull/1645 */ function useSSRQueryOptionsIfNeeded(queryKey, opts) { const { queryClient, ssrState } = useContext(); return ssrState && ssrState !== 'mounted' && queryClient.getQueryCache().find({ queryKey })?.state.status === 'error' ? { retryOnMount: false, ...opts } : opts; } function useQuery$1(path, input, opts) { const context = useContext(); const { abortOnUnmount, client, ssrState, queryClient, prefetchQuery } = context; const queryKey = getQueryKeyInternal(path, input, 'query'); const defaultOpts = queryClient.getQueryDefaults(queryKey); const isInputSkipToken = input === skipToken; if (typeof window === 'undefined' && ssrState === 'prepass' && opts?.trpc?.ssr !== false && (opts?.enabled ?? defaultOpts?.enabled) !== false && !isInputSkipToken && !queryClient.getQueryCache().find({ queryKey })) { void prefetchQuery(queryKey, opts); } const ssrOpts = useSSRQueryOptionsIfNeeded(queryKey, { ...defaultOpts, ...opts }); const shouldAbortOnUnmount = opts?.trpc?.abortOnUnmount ?? config?.abortOnUnmount ?? abortOnUnmount; const hook = useQuery({ ...ssrOpts, queryKey: queryKey, queryFn: isInputSkipToken ? input : async (queryFunctionContext)=>{ const actualOpts = { ...ssrOpts, trpc: { ...ssrOpts?.trpc, ...shouldAbortOnUnmount ? { signal: queryFunctionContext.signal } : { signal: null } } }; const result = await client.query(...getClientArgs(queryKey, actualOpts)); if (isAsyncIterable(result)) { return buildQueryFromAsyncIterable(result, queryClient, queryKey); } return result; } }, queryClient); hook.trpc = useHookResult({ path }); return hook; } function usePrefetchQuery$1(path, input, opts) { const context = useContext(); const queryKey = getQueryKeyInternal(path, input, 'query'); const isInputSkipToken = input === skipToken; const shouldAbortOnUnmount = opts?.trpc?.abortOnUnmount ?? config?.abortOnUnmount ?? context.abortOnUnmount; usePrefetchQuery({ ...opts, queryKey: queryKey, queryFn: isInputSkipToken ? input : (queryFunctionContext)=>{ const actualOpts = { trpc: { ...opts?.trpc, ...shouldAbortOnUnmount ? { signal: queryFunctionContext.signal } : {} } }; return context.client.query(...getClientArgs(queryKey, actualOpts)); } }); } function useSuspenseQuery$1(path, input, opts) { const context = useContext(); const queryKey = getQueryKeyInternal(path, input, 'query'); const shouldAbortOnUnmount = opts?.trpc?.abortOnUnmount ?? config?.abortOnUnmount ?? context.abortOnUnmount; const hook = useSuspenseQuery({ ...opts, queryKey: queryKey, queryFn: (queryFunctionContext)=>{ const actualOpts = { ...opts, trpc: { ...opts?.trpc, ...shouldAbortOnUnmount ? { signal: queryFunctionContext.signal } : { signal: null } } }; return context.client.query(...getClientArgs(queryKey, actualOpts)); } }, context.queryClient); hook.trpc = useHookResult({ path }); return [ hook.data, hook ]; } function useMutation$1(path, opts) { const { client, queryClient } = useContext(); const mutationKey = getMutationKeyInternal(path); const defaultOpts = queryClient.defaultMutationOptions(queryClient.getMutationDefaults(mutationKey)); const hook = useMutation({ ...opts, mutationKey: mutationKey, mutationFn: (input)=>{ return client.mutation(...getClientArgs([ path, { input } ], opts)); }, onSuccess (...args) { const originalFn = ()=>opts?.onSuccess?.(...args) ?? defaultOpts?.onSuccess?.(...args); return mutationSuccessOverride({ originalFn, queryClient, meta: opts?.meta ?? defaultOpts?.meta ?? {} }); } }, queryClient); hook.trpc = useHookResult({ path }); return hook; } const initialStateIdle = { data: undefined, error: null, status: 'idle' }; const initialStateConnecting = { data: undefined, error: null, status: 'connecting' }; /* istanbul ignore next -- @preserve */ function useSubscription(path, input, opts) { const enabled = opts?.enabled ?? input !== skipToken; const queryKey = hashKey(getQueryKeyInternal(path, input, 'any')); const { client } = useContext(); const optsRef = React.useRef(opts); React.useEffect(()=>{ optsRef.current = opts; }); const [trackedProps] = React.useState(new Set([])); const addTrackedProp = React.useCallback((key)=>{ trackedProps.add(key); }, [ trackedProps ]); const currentSubscriptionRef = React.useRef(null); const updateState = React.useCallback((callback)=>{ const prev = resultRef.current; const next = resultRef.current = callback(prev); let shouldUpdate = false; for (const key of trackedProps){ if (prev[key] !== next[key]) { shouldUpdate = true; break; } } if (shouldUpdate) { setState(trackResult(next, addTrackedProp)); } }, [ addTrackedProp, trackedProps ]); const reset = React.useCallback(()=>{ // unsubscribe from the previous subscription currentSubscriptionRef.current?.unsubscribe(); if (!enabled) { updateState(()=>({ ...initialStateIdle, reset })); return; } updateState(()=>({ ...initialStateConnecting, reset })); const subscription = client.subscription(path.join('.'), input ?? undefined, { onStarted: ()=>{ optsRef.current.onStarted?.(); updateState((prev)=>({ ...prev, status: 'pending', error: null })); }, onData: (data)=>{ optsRef.current.onData?.(data); updateState((prev)=>({ ...prev, status: 'pending', data, error: null })); }, onError: (error)=>{ optsRef.current.onError?.(error); updateState((prev)=>({ ...prev, status: 'error', error })); }, onConnectionStateChange: (result)=>{ updateState((prev)=>{ switch(result.state){ case 'idle': return { ...prev, status: result.state, error: null, data: undefined }; case 'connecting': return { ...prev, error: result.error, status: result.state }; case 'pending': // handled when data is / onStarted return prev; } }); }, onComplete: ()=>{ optsRef.current.onComplete?.(); // In the case of WebSockets, the connection might not be idle so `onConnectionStateChange` will not be called until the connection is closed. // In this case, we need to set the state to idle manually. updateState((prev)=>({ ...prev, status: 'idle', error: null, data: undefined })); // (We might want to add a `connectionState` to the state to track the connection state separately) } }); currentSubscriptionRef.current = subscription; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ client, queryKey, enabled, updateState ]); React.useEffect(()=>{ reset(); return ()=>{ currentSubscriptionRef.current?.unsubscribe(); }; }, [ reset ]); const resultRef = React.useRef(enabled ? { ...initialStateConnecting, reset } : { ...initialStateIdle, reset }); const [state, setState] = React.useState(trackResult(resultRef.current, addTrackedProp)); return state; } function useInfiniteQuery$1(path, input, opts) { const { client, ssrState, prefetchInfiniteQuery, queryClient, abortOnUnmount } = useContext(); const queryKey = getQueryKeyInternal(path, input, 'infinite'); const defaultOpts = queryClient.getQueryDefaults(queryKey); const isInputSkipToken = input === skipToken; if (typeof window === 'undefined' && ssrState === 'prepass' && opts?.trpc?.ssr !== false && (opts?.enabled ?? defaultOpts?.enabled) !== false && !isInputSkipToken && !queryClient.getQueryCache().find({ queryKey })) { void prefetchInfiniteQuery(queryKey, { ...defaultOpts, ...opts }); } const ssrOpts = useSSRQueryOptionsIfNeeded(queryKey, { ...defaultOpts, ...opts }); // request option should take priority over global const shouldAbortOnUnmount = opts?.trpc?.abortOnUnmount ?? abortOnUnmount; const hook = useInfiniteQuery({ ...ssrOpts, initialPageParam: opts.initialCursor ?? null, persister: opts.persister, queryKey: queryKey, queryFn: isInputSkipToken ? input : (queryFunctionContext)=>{ const actualOpts = { ...ssrOpts, trpc: { ...ssrOpts?.trpc, ...shouldAbortOnUnmount ? { signal: queryFunctionContext.signal } : { signal: null } } }; return client.query(...getClientArgs(queryKey, actualOpts, { pageParam: queryFunctionContext.pageParam ?? opts.initialCursor, direction: queryFunctionContext.direction })); } }, queryClient); hook.trpc = useHookResult({ path }); return hook; } function usePrefetchInfiniteQuery$1(path, input, opts) { const context = useContext(); const queryKey = getQueryKeyInternal(path, input, 'infinite'); const defaultOpts = context.queryClient.getQueryDefaults(queryKey); const isInputSkipToken = input === skipToken; const ssrOpts = useSSRQueryOptionsIfNeeded(queryKey, { ...defaultOpts, ...opts }); // request option should take priority over global const shouldAbortOnUnmount = opts?.trpc?.abortOnUnmount ?? context.abortOnUnmount; usePrefetchInfiniteQuery({ ...opts, initialPageParam: opts.initialCursor ?? null, queryKey, queryFn: isInputSkipToken ? input : (queryFunctionContext)=>{ const actualOpts = { ...ssrOpts, trpc: { ...ssrOpts?.trpc, ...shouldAbortOnUnmount ? { signal: queryFunctionContext.signal } : {} } }; return context.client.query(...getClientArgs(queryKey, actualOpts, { pageParam: queryFunctionContext.pageParam ?? opts.initialCursor, direction: queryFunctionContext.direction })); } }); } function useSuspenseInfiniteQuery$1(path, input, opts) { const context = useContext(); const queryKey = getQueryKeyInternal(path, input, 'infinite'); const defaultOpts = context.queryClient.getQueryDefaults(queryKey); const ssrOpts = useSSRQueryOptionsIfNeeded(queryKey, { ...defaultOpts, ...opts }); // request option should take priority over global const shouldAbortOnUnmount = opts?.trpc?.abortOnUnmount ?? context.abortOnUnmount; const hook = useSuspenseInfiniteQuery({ ...opts, initialPageParam: opts.initialCursor ?? null, queryKey, queryFn: (queryFunctionContext)=>{ const actualOpts = { ...ssrOpts, trpc: { ...ssrOpts?.trpc, ...shouldAbortOnUnmount ? { signal: queryFunctionContext.signal } : {} } }; return context.client.query(...getClientArgs(queryKey, actualOpts, { pageParam: queryFunctionContext.pageParam ?? opts.initialCursor, direction: queryFunctionContext.direction })); } }, context.queryClient); hook.trpc = useHookResult({ path }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return [ hook.data, hook ]; } const useQueries$1 = (queriesCallback, options)=>{ const { ssrState, queryClient, prefetchQuery, client } = useContext(); const proxy = createUseQueries(client); const queries = queriesCallback(proxy); if (typeof window === 'undefined' && ssrState === 'prepass') { for (const query of queries){ const queryOption = query; if (queryOption.trpc?.ssr !== false && !queryClient.getQueryCache().find({ queryKey: queryOption.queryKey })) { void prefetchQuery(queryOption.queryKey, queryOption); } } } return useQueries({ queries: queries.map((query)=>({ ...query, queryKey: query.queryKey })), combine: options?.combine }, queryClient); }; const useSuspenseQueries$1 = (queriesCallback)=>{ const { queryClient, client } = useContext(); const proxy = createUseQueries(client); const queries = queriesCallback(proxy); const hook = useSuspenseQueries({ queries: queries.map((query)=>({ ...query, queryFn: query.queryFn, queryKey: query.queryKey })) }, queryClient); return [ hook.map((h)=>h.data), hook ]; }; return { Provider: TRPCProvider, createClient, useContext, useUtils: useContext, useQuery: useQuery$1, usePrefetchQuery: usePrefetchQuery$1, useSuspenseQuery: useSuspenseQuery$1, useQueries: useQueries$1, useSuspenseQueries: useSuspenseQueries$1, useMutation: useMutation$1, useSubscription, useInfiniteQuery: useInfiniteQuery$1, usePrefetchInfiniteQuery: usePrefetchInfiniteQuery$1, useSuspenseInfiniteQuery: useSuspenseInfiniteQuery$1 }; } export { createRootHooks };