React Hook Form

@beignet/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 @beignet/react-hook-form react-hook-form @hookform/resolvers

Setup

import { createReactHookForm } from "@beignet/react-hook-form";
import { createTodo } from "@/features/todos/contracts";

const rhf = createReactHookForm();
const createTodoForm = rhf(createTodo);

Bind the contract builder directly with rhf(contract). Use contract.config only for integration code that cannot accept the builder.

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,
});