Files
solid-trpc/src/shared/hooks/createHooksInternal.tsx
T
2022-11-01 10:11:13 +02:00

654 lines
18 KiB
TypeScript

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<TData, TCursor = any> = {
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<TRPCRequestOptions, "signal"> {
/**
* 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<TPath, TInput, TOutput, TData, TError>
extends CreateQueryOptions<TOutput, TError, TData, () => [TPath, TInput]>,
TRPCUseQueryBaseOptions {}
export interface UseTRPCInfiniteQueryOptions<TPath, TInput, TOutput, TError>
extends CreateInfiniteQueryOptions<
TOutput,
TError,
TOutput,
TOutput,
() => [TPath, TInput]
>,
TRPCUseQueryBaseOptions {}
export interface UseTRPCMutationOptions<
TInput,
TError,
TOutput,
TContext = unknown
> extends CreateMutationOptions<TOutput, TError, TInput, TContext>,
TRPCUseQueryBaseOptions {}
export interface UseTRPCSubscriptionOptions<TOutput, TError> {
enabled?: boolean;
onStarted?: () => void;
onData: (data: TOutput) => void;
onError?: (err: TError) => void;
}
function getClientArgs<TPathAndInput extends unknown[], TOptions>(
pathAndInput: TPathAndInput,
opts: TOptions
) {
const [path, input] = pathAndInput;
return [path, input, (opts as any)?.trpc] as const;
}
type inferInfiniteQueryNames<TObj extends ProcedureRecord> = {
[TPath in keyof TObj]: inferProcedureInput<TObj[TPath]> extends {
cursor?: any;
}
? TPath
: never;
}[keyof TObj];
type inferProcedures<TObj extends ProcedureRecord> = {
[TPath in keyof TObj]: {
input: inferProcedureInput<TObj[TPath]>;
output: inferProcedureOutput<TObj[TPath]>;
};
};
export interface TRPCProviderProps<TRouter extends AnyRouter, TSSRContext>
extends TRPCContextProps<TRouter, TSSRContext> {
children: JSX.Element;
}
export type TRPCProvider<TRouter extends AnyRouter, TSSRContext> = (
props: TRPCProviderProps<TRouter, TSSRContext> & {
queryClientOpts?: Omit<QueryClientProviderProps, "client">;
}
) => JSX.Element;
export type UseDehydratedState<TRouter extends AnyRouter> = (
client: TRPCClient<TRouter>,
trpcState: DehydratedState | undefined
) => Accessor<DehydratedState | undefined>;
export type CreateClient<TRouter extends AnyRouter> = (
opts: CreateTRPCClientOptions<TRouter>
) => TRPCClient<TRouter>;
interface TRPCHookResult {
trpc: {
path: string;
};
}
/**
* @internal
*/
export type UseTRPCQueryResult<TData, TError> = CreateQueryResult<
TData,
TError
> &
TRPCHookResult;
/**
* @internal
*/
export type UseTRPCInfiniteQueryResult<TData, TError> =
CreateInfiniteQueryResult<TData, TError> & TRPCHookResult;
/**
* @internal
*/
export type UseTRPCMutationResult<TData, TError, TVariables, TContext> =
CreateMutationResult<TData, TError, TVariables, TContext> & 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<TRouter>) {
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<TRouter>;
type TInfiniteQueryNames = inferInfiniteQueryNames<TQueries>;
type TQueryValues = inferProcedures<TQueries>;
type TMutationValues = inferProcedures<TMutations>;
type ProviderContext = Omit<
TRPCContextState<TRouter, TSSRContext>,
"ssrState"
> & {
ssrState: Accessor<TRPCContextState<TRouter, TSSRContext>["ssrState"]>;
};
const Context = (config?.context ?? TRPCContext) as Context<ProviderContext>;
const SolidQueryContext = config?.solidQueryContext as Context<
QueryClient | undefined
>;
const createClient: CreateClient<TRouter> = (opts) => {
return createTRPCClient(opts);
};
const TRPCProvider: TRPCProvider<TRouter, TSSRContext> = (props) => {
const { abortOnUnmount = false, client, queryClient, ssrContext } = props;
const [ssrState, setSSRState] = createSignal<SSRState>(
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 (
<Context.Provider
value={{
abortOnUnmount,
queryClient,
client,
ssrContext: ssrContext || null,
ssrState,
fetchQuery: (pathAndInput, opts) => {
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
);
},
}}
>
<QueryClientProvider
client={queryClient}
{...((props.queryClientOpts ?? {}) as any)}
>
{props.children}
</QueryClientProvider>
</Context.Provider>
);
};
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<TQueries[TPath]>],
opts?: UseTRPCQueryOptions<
TPath,
TQueryValues[TPath]["input"],
TQueryFnData,
TData,
TError
>
): UseTRPCQueryResult<TData, TError> {
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<TData, TError>;
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<TRouter, TPath>
>(
pathAndInput: [
path: TPath,
...args: inferHandlerInput<TSubscriptions[TPath]>
],
opts: UseTRPCSubscriptionOptions<
inferObservableValue<inferProcedureOutput<TSubscriptions[TPath]>>,
inferProcedureClientError<TSubscriptions[TPath]>
>
) {
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<TRouter["_def"]["subscriptions"][TPath]>
>(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<TPath extends TInfiniteQueryNames & string>(
pathAndInput: [
path: TPath,
input: Omit<TQueryValues[TPath]["input"], "cursor">
],
opts?: UseTRPCInfiniteQueryOptions<
TPath,
Omit<TQueryValues[TPath]["input"], "cursor">,
TQueryValues[TPath]["output"],
TError
>
): UseTRPCInfiniteQueryResult<TQueryValues[TPath]["output"], TError> {
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<TQueryValues[TPath]["output"], TError>;
hook.trpc = useHookResult({
path,
});
return hook;
}
const useDehydratedState: UseDehydratedState<TRouter> = (
client,
trpcState
) => {
const transformed: Accessor<DehydratedState | undefined> = 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<TRouter extends AnyRouter, TSSRContext = unknown> {
fn() {
return createHooksInternal<TRouter, TSSRContext>();
}
}
type returnTypeInferer<TType> = TType extends (
a: Record<string, string>
) => infer U
? U
: never;
type fooType<TRouter extends AnyRouter, TSSRContext = unknown> = GnClass<
TRouter,
TSSRContext
>["fn"];
/**
* Infer the type of a `createSolidQueryHooks` function
* @internal
*/
export type CreateSolidQueryHooks<
TRouter extends AnyRouter,
TSSRContext = unknown
> = returnTypeInferer<fooType<TRouter, TSSRContext>>;