React Hook Form integration for Beignet
This package provides automatic form validation using your contract's body schema. Works with any Standard Schema library (Zod, Valibot, ArkType, etc.).
npm install @beignet/react-hook-form @beignet/core react-hook-form @hookform/resolvers react
This package requires TypeScript 5.0 or higher for proper type inference.
import { createReactHookForm } from "@beignet/react-hook-form";
import { createTodo } from "@/features/todos/contracts";
const rhf = createReactHookForm();
function CreateTodoForm() {
const { useForm } = rhf(createTodo);
const form = useForm({
defaultValues: {
title: "",
completed: false,
},
});
const onSubmit = form.handleSubmit((values) => {
// values is typed as: { title: string; completed?: boolean }
console.log("Creating todo:", values);
});
return (
<form onSubmit={onSubmit}>
<input
{...form.register("title")}
placeholder="What needs to be done?"
/>
{form.formState.errors.title && (
<p className="error">{form.formState.errors.title.message}</p>
)}
<label>
<input type="checkbox" {...form.register("completed")} />
Completed
</label>
<button type="submit" disabled={form.formState.isSubmitting}>
Create Todo
</button>
</form>
);
}
import { createReactHookForm } from "@beignet/react-hook-form";
import { useMutation } from "@tanstack/react-query";
import { rq } from "@/client/rq";
import { createTodo } from "@/features/todos/contracts";
const rhf = createReactHookForm();
function CreateTodoForm() {
const { useForm } = rhf(createTodo);
const form = useForm({
defaultValues: { title: "" },
});
const mutation = useMutation(
rq(createTodo).mutationOptions()
);
const onSubmit = form.handleSubmit((values) => {
mutation.mutate({ body: values });
});
return (
<form onSubmit={onSubmit}>
<input {...form.register("title")} placeholder="Title" />
{form.formState.errors.title && (
<p>{form.formState.errors.title.message}</p>
)}
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Creating..." : "Create"}
</button>
{mutation.isError && (
<p className="error">{mutation.error.message}</p>
)}
</form>
);
}
If you need to disable the schema resolver (e.g., for partial form handling):
const { useForm } = rhf(createTodo);
const form = useForm({
resolverEnabled: false, // Disable schema validation
defaultValues: { title: "" },
});
You can pass either a contract builder or its config:
import { createReactHookForm } from "@beignet/react-hook-form";
import { createTodo } from "@/features/todos/contracts";
const rhf = createReactHookForm();
// Using ContractBuilder directly
const { useForm } = rhf(createTodo);
// Or using the contract config
const { useForm } = rhf(createTodo.config);
createReactHookForm()Creates a React Hook Form adapter factory.
const rhf = createReactHookForm();
rhf(contract)Creates a React Hook Form adapter for a contract.
const adapter = rhf(createTodo);
adapter.useForm(props?)Returns a React Hook Form useForm result with the contract's body schema as resolver.
const form = adapter.useForm({
defaultValues?: { ... },
resolverEnabled?: boolean, // default: true
// ...other React Hook Form options
});
Form values are automatically typed based on the contract's body schema:
// Contract definition
const createTodo = todos
.post("/api/todos")
.body(z.object({
title: z.string().min(1),
description: z.string().optional(),
completed: z.boolean().optional(),
}))
.responses({ 201: TodoSchema });
// Form values are inferred
const rhf = createReactHookForm();
const form = rhf(createTodo).useForm();
form.register("title"); // ✓ Valid
form.register("description"); // ✓ Valid
form.register("invalid"); // ✗ Type error
This package uses the @hookform/resolvers/standard-schema resolver, which works with any Standard Schema compatible library:
z.object({ ... })v.object({ ... })type({ ... })The resolver validates:
Validation errors are available via form.formState.errors:
{form.formState.errors.title && (
<span className="error">
{form.formState.errors.title.message}
</span>
)}
import { createReactHookForm } from "@beignet/react-hook-form";
import { useMutation } from "@tanstack/react-query";
import { rq } from "@/client/rq";
import { updateProfile } from "@/features/profile/contracts";
const rhf = createReactHookForm();
function ProfileForm({ profile }) {
const { useForm } = rhf(updateProfile);
const form = useForm({
defaultValues: {
name: profile.name,
email: profile.email,
bio: profile.bio ?? "",
},
});
const mutation = useMutation(
rq(updateProfile).mutationOptions({
onSuccess: () => {
toast.success("Profile updated!");
},
})
);
const onSubmit = form.handleSubmit((values) => {
mutation.mutate({ body: values });
});
const { errors, isDirty, isSubmitting } = form.formState;
return (
<form onSubmit={onSubmit}>
<div>
<label htmlFor="name">Name</label>
<input id="name" {...form.register("name")} />
{errors.name && <span className="error">{errors.name.message}</span>}
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" type="email" {...form.register("email")} />
{errors.email && <span className="error">{errors.email.message}</span>}
</div>
<div>
<label htmlFor="bio">Bio</label>
<textarea id="bio" {...form.register("bio")} />
{errors.bio && <span className="error">{errors.bio.message}</span>}
</div>
<button type="submit" disabled={!isDirty || isSubmitting}>
{isSubmitting ? "Saving..." : "Save Changes"}
</button>
</form>
);
}
@beignet/core/contracts - Core contract definitions@beignet/react-query - TanStack Query integration@beignet/core/client - HTTP clientMIT