useFormAction

Wraps a server function for form-specific usage. Handles both FormData and plain object inputs, tracks submission state, and provides the raw FormData for optimistic UI updates.

Import

import { useFormAction } from "catmint/hooks";

Signature

function useFormAction<TInput, TOutput>(
  fn: (input: TInput) => Promise<TOutput>,
): FormActionState<TInput, TOutput>;
interface FormActionState<TInput, TOutput> {
  submit: (data: FormData | TInput) => Promise<TOutput>;
  data: TOutput | undefined;
  error: Error | undefined;
  pending: boolean;
  formData: FormData | undefined;
  reset: () => void;
}

Parameters

ParameterTypeRequiredDescription
fn(input: TInput) => Promise<TOutput>YesA server function to handle the form submission. When FormData is passed to submit, it is automatically converted to a plain object before calling fn.

Return Value

PropertyTypeDescription
submit(data: FormData | TInput) => Promise<TOutput>Submit the form. Accepts either a FormData object (from a form event) or a plain object matching TInput.
dataTOutput | undefinedThe most recent successful result.
errorError | undefinedThe most recent error, cleared on the next submission.
pendingbooleantrue while the submission is in flight.
formDataFormData | undefinedThe raw FormData from the most recent submission (only set when FormData was passed to submit). Useful for optimistic UI.
reset() => voidResets all state (data, error, pending, formData) to initial values.

Examples

Basic form submission

// ContactForm.client.tsx
import { useFormAction } from "catmint/hooks";
import { submitContactForm } from "./actions";

function ContactForm() {
  const { submit, pending, error, data } = useFormAction(submitContactForm);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    await submit(new FormData(e.currentTarget));
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button disabled={pending}>{pending ? "Sending..." : "Send"}</button>
      {error && <p className="error">{error.message}</p>}
      {data && <p className="success">Message sent!</p>}
    </form>
  );
}

With plain object input

const { submit } = useFormAction(createUser);

// You can also pass a plain object instead of FormData
await submit({ name: "Alice", email: "alice@example.com" });

Optimistic UI with formData

const { submit, pending, formData } = useFormAction(addComment);

// Show the pending comment optimistically
{
  pending && formData && (
    <div className="comment pending">{formData.get("body") as string}</div>
  );
}

See Also