@trpc/react-query
Version:
523 lines (520 loc) • 20.8 kB
JavaScript
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 };