38 lines · 1.2 KB
import { useRouter } from "@tanstack/react-router";
import { useOptimistic, useState, useTransition } from "react";

function messageOf(error: unknown): string {
  return error instanceof Error && error.message ? error.message : "Something went wrong";
}

/**
 * Applies a mutation's expected result immediately and keeps it until the server call and the
 * loader refresh both finish, so toggles like star/follow/watch never flash the stale value.
 * On failure the optimistic value is dropped automatically and the error is exposed.
 */
export function useOptimisticMutation<TState, TInput>(
  serverState: TState,
  reduce: (state: TState, input: TInput) => TState,
  action: (input: TInput) => Promise<unknown>,
) {
  const router = useRouter();
  const [state, apply] = useOptimistic(serverState, reduce);
  const [pending, startTransition] = useTransition();
  const [error, setError] = useState<string | null>(null);

  function run(input: TInput) {
    setError(null);
    startTransition(async () => {
      apply(input);
      try {
        await action(input);
        await router.invalidate();
      } catch (caught) {
        setError(messageOf(caught));
      }
    });
  }

  return { state, run, pending, error };
}