import { useMutation } from "@tanstack/react-query"; import { useRouter } from "@tanstack/react-router"; import { errorMessage } from "~/lib/error-message"; import { paymentDetail, showUpsell } from "~/lib/upsell"; /** * Runs a server-function mutation, tracks its pending/error state and refreshes route loaders on * success. Route loaders are the source of truth for page data, so `router.invalidate({ sync: true })` * is the invalidation step; `sync` makes it wait for the loaders, so `pending` covers the refresh and * callers never see stale data after `run` resolves. Hooks that own react-query state invalidate * their own keys as well. */ export function useAction( action: (input: TInput) => Promise, options: { invalidate?: boolean } = {}, ) { const router = useRouter(); const invalidate = options.invalidate ?? true; const mutation = useMutation({ mutationFn: action, onSuccess: async () => { if (invalidate) await router.invalidate({ sync: true }); }, // Out-of-budget errors open the upsell dialog everywhere, on top of the inline error message. onError: (error) => { const payment = paymentDetail(error); if (payment) showUpsell(payment); }, }); async function run(input: TInput): Promise { try { return await mutation.mutateAsync(input); } catch { return undefined; } } return { run, pending: mutation.isPending, error: mutation.isError ? errorMessage(mutation.error) : null, reset: mutation.reset, }; }