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<TInput, TResult>(
action: (input: TInput) => Promise<TResult>,
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<TResult | undefined> {
try {
return await mutation.mutateAsync(input);
} catch {
return undefined;
}
}
return {
run,