React Hook Form

@contract-kit/react-hook-form creates typed React Hook Form options from a contract body schema. Use it when a form submits to a contract and should reuse the same validation rules on the client.

bun add @contract-kit/react-hook-form react-hook-form @hookform/resolvers

Setup

import { rhf } from "@contract-kit/react-hook-form";
import { createTodo } from "@/app/contracts/todo";

const createTodoForm = rhf(createTodo);

Basic form

function CreateTodoForm() {
  const form = createTodoForm.useForm({
    defaultValues: { title: "", completed: false },
  });

  const onSubmit = form.handleSubmit(async (data) => {
    await createTodoEndpoint.call({ body: data });
  });

  return (
    <form onSubmit={onSubmit}>
      <input {...form.register("title")} />

      {form.formState.errors.title && (
        <span>{form.formState.errors.title.message}</span>
      )}

      <button type="submit" disabled={form.formState.isSubmitting}>
        Create
      </button>
    </form>
  );
}

Validation runs automatically using your contract's body schema. Field names, values, and error messages are inferred from the contract.

With React Query

function CreateTodoForm() {
  const form = createTodoForm.useForm({
    defaultValues: { title: "", completed: false },
  });

  const mutation = useMutation(
    rq(createTodo).mutationOptions({
      onSuccess: () => form.reset(),
      onError: (error) => {
        form.setError("root", {
          message: error.body?.message ?? error.message,
        });
      },
    }),
  );

  const onSubmit = form.handleSubmit((data) => {
    mutation.mutate({ body: data });
  });

  return (
    <form onSubmit={onSubmit}>
      <input {...form.register("title")} />
      {form.formState.errors.title && (
        <span>{form.formState.errors.title.message}</span>
      )}
      {form.formState.errors.root && (
        <span>{form.formState.errors.root.message}</span>
      )}
      <button type="submit" disabled={mutation.isPending}>
        Create
      </button>
    </form>
  );
}

Form options

Get raw form options if you want to call useForm yourself.

import { useForm } from "react-hook-form";

const form = useForm(
  createTodoForm.formOptions({
    defaultValues: { title: "" },
    mode: "onBlur",
  }),
);

Disable automatic validation

Set resolverEnabled to false when you want React Hook Form typing without the schema resolver.

const form = createTodoForm.useForm({
  resolverEnabled: false,
});