import type { CreateTRPCClientOptions, TRPCLink, TRPCProcedureOptions, TRPCRequestOptions, } from '@trpc/client'; import { createTRPCUntypedClient, TRPCClientError } from '@trpc/client'; import type { CoercedTransformerParameters, TransformerOptions, } from '@trpc/client/unstable-internals'; import { getTransformer } from '@trpc/client/unstable-internals'; import { observable } from '@trpc/server/observable'; import type { inferClientTypes, InferrableClientTypes, MaybePromise, Simplify, TypeError, } from '@trpc/server/unstable-core-do-not-import'; import { transformResult } from '@trpc/server/unstable-core-do-not-import'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { TRPCActionHandler } from './server'; import type { ActionHandlerDef } from './shared'; import { isFormData } from './shared'; type MutationArgs = TDef['input'] extends void ? [input?: undefined | void, opts?: TRPCProcedureOptions] : [input: FormData | TDef['input'], opts?: TRPCProcedureOptions]; interface UseTRPCActionBaseResult { mutate: (...args: MutationArgs) => void; mutateAsync: (...args: MutationArgs) => Promise; } interface UseTRPCActionSuccessResult< TDef extends ActionHandlerDef, > extends UseTRPCActionBaseResult { data: TDef['output']; error?: never; status: 'success'; } interface UseTRPCActionErrorResult< TDef extends ActionHandlerDef, > extends UseTRPCActionBaseResult { data?: never; error: TRPCClientError; status: 'error'; } interface UseTRPCActionIdleResult< TDef extends ActionHandlerDef, > extends UseTRPCActionBaseResult { data?: never; error?: never; status: 'idle'; } interface UseTRPCActionLoadingResult< TDef extends ActionHandlerDef, > extends UseTRPCActionBaseResult { data?: never; error?: never; status: 'loading'; } // ts-prune-ignore-next export type UseTRPCActionResult = | UseTRPCActionErrorResult | UseTRPCActionIdleResult | UseTRPCActionLoadingResult | UseTRPCActionSuccessResult; type ActionContext = { _action: (...args: any[]) => Promise; }; // ts-prune-ignore-next export function experimental_serverActionLink< TInferrable extends InferrableClientTypes, >( ...args: InferrableClientTypes extends TInferrable ? [ TypeError<'Generic parameter missing in `experimental_createActionHook()` or experimental_serverActionLink()'>, ] : inferClientTypes['transformer'] extends true ? [ opts: TransformerOptions<{ transformer: true; }>, ] : [ opts?: TransformerOptions<{ transformer: false; }>, ] ): TRPCLink { const [opts] = args as [CoercedTransformerParameters]; const transformer = getTransformer(opts?.transformer); return () => ({ op }) => observable((observer) => { const context = op.context as ActionContext; context ._action( isFormData(op.input) ? op.input : transformer.input.serialize(op.input), ) .then((data) => { const transformed = transformResult(data, transformer.output); if (!transformed.ok) { observer.error(TRPCClientError.from(transformed.error, {})); return; } observer.next({ context: op.context, result: transformed.result, }); observer.complete(); }) .catch((cause) => { observer.error(TRPCClientError.from(cause)); }); }); } interface UseTRPCActionOptions { onSuccess?: (result: TDef['output']) => MaybePromise | void; onError?: (result: TRPCClientError) => MaybePromise; } // ts-prune-ignore-next export function experimental_createActionHook< TInferrable extends InferrableClientTypes, >( opts: InferrableClientTypes extends TInferrable ? TypeError<'Generic parameter missing in `experimental_createActionHook()`'> : CreateTRPCClientOptions, ) { type ActionContext = { _action: (...args: any[]) => Promise; }; const client = createTRPCUntypedClient( opts as Exclude>, ); return function useAction( handler: TRPCActionHandler, useActionOpts?: UseTRPCActionOptions>, ) { const count = useRef(0); type Result = UseTRPCActionResult; type State = Omit; const [state, setState] = useState({ status: 'idle', }); const actionOptsRef = useRef(useActionOpts); actionOptsRef.current = useActionOpts; useEffect(() => { return () => { // cleanup after unmount to prevent calling hook opts after unmount count.current = -1; actionOptsRef.current = undefined; }; }, []); const mutateAsync = useCallback( (input: any, requestOptions?: TRPCRequestOptions) => { const idx = ++count.current; const context: ActionContext = { ...requestOptions?.context, _action(innerInput) { return handler(innerInput); }, }; setState({ status: 'loading', }); return client .mutation('serverAction', input, { ...requestOptions, context, }) .then(async (data) => { await actionOptsRef.current?.onSuccess?.(data); if (idx !== count.current) { return; } setState({ status: 'success', data, }); }) .catch(async (error) => { await actionOptsRef.current?.onError?.(error); throw error; }) .catch((error) => { if (idx !== count.current) { return; } setState({ status: 'error', error: TRPCClientError.from(error, {}), }); throw error; }); }, [handler], ) as Result['mutateAsync']; const mutate: Result['mutate'] = useCallback( (...args: any[]) => { void (mutateAsync as any)(...args).catch(() => { // ignored }); }, [mutateAsync], ); return useMemo( () => ({ ...state, mutate, mutateAsync, }), [mutate, mutateAsync, state], ) as Result; }; }