The single place `react-hook-form` is imported directly in this repo. Everything else should import form state/control from here instead of `react-hook-form` — that keeps the underlying library swappable from one spot later.

pnpm add @behivetech/forms.form-provider

This is the snapshot of v0.1.9, the current version, as recorded on 2026-11-10. The live page runs its examples →

registry/behivetech/forms/form-provider at v0.1.9, snapshotted 2026-11-10

form-provider.tsx
1import { type ReactNode } from "react";
2import {
3 FormProvider as RHFFormProvider,
4 type FieldValues,
5 type UseFormReturn,
6} from "react-hook-form";
7
8export interface FormProviderProps<
9 TFieldValues extends FieldValues = FieldValues,
10> {
11 /** The return value of `useForm()` (re-exported from this package) */
12 methods: UseFormReturn<TFieldValues>;
13 /** Descendants that will consume the form context via `useFormContext()` */
14 children: ReactNode;
15}
16
17/**
18 * Thin wrapper around react-hook-form's `FormProvider`. Import this (and
19 * `useForm`, `Controller`, etc.) from this package instead of importing
20 * `react-hook-form` directly anywhere else — this is the one place that
21 * knows about the underlying form library, so it can be swapped later
22 * without touching every consumer.
23 */
24export const FormProvider = <TFieldValues extends FieldValues = FieldValues>({
25 methods,
26 children,
27}: FormProviderProps<TFieldValues>) => {
28 return <RHFFormProvider {...methods}>{children}</RHFFormProvider>;
29};
form.tsx
1import { type FormHTMLAttributes, type ReactNode } from "react";
2import {
3 type FieldValues,
4 type SubmitHandler,
5 type UseFormReturn,
6} from "react-hook-form";
7import { FormProvider } from "./form-provider.js";
8
9export interface FormProps<
10 TFieldValues extends FieldValues = FieldValues,
11> extends Omit<FormHTMLAttributes<HTMLFormElement>, "onSubmit"> {
12 /** The return value of `useForm()` */
13 methods: UseFormReturn<TFieldValues>;
14 /** Called with the validated values when the form is submitted */
15 onSubmit: SubmitHandler<TFieldValues>;
16 /** Form content, typically FormField-wrapped controls */
17 children: ReactNode;
18}
19
20/**
21 * Convenience wrapper for the common case: a `<form>` whose submit is wired
22 * to react-hook-form's `handleSubmit`, with `FormProvider` already applied
23 * so descendants can use `useFormContext()`. For editing UIs with no submit
24 * moment (live/instant field-by-field updates), use the bare atoms +
25 * `FormField` instead — this component assumes a real submit action exists.
26 */
27export const Form = <TFieldValues extends FieldValues = FieldValues>({
28 methods,
29 onSubmit,
30 children,
31 ...rest
32}: FormProps<TFieldValues>) => {
33 return (
34 <FormProvider methods={methods}>
35 <form {...rest} onSubmit={methods.handleSubmit(onSubmit)}>
36 {children}
37 </form>
38 </FormProvider>
39 );
40};
index.ts
1export { FormProvider } from "./form-provider.js";
2export type { FormProviderProps } from "./form-provider.js";
3
4export { Form } from "./form.js";
5export type { FormProps } from "./form.js";
6
7export {
8 useForm,
9 useFormContext,
10 useController,
11 useFormState,
12 useWatch,
13 Controller,
14} from "react-hook-form";
15export type {
16 UseFormReturn,
17 UseFormProps,
18 FieldValues,
19 SubmitHandler,
20 Control,
21 ControllerProps,
22 ControllerRenderProps,
23 ControllerFieldState,
24 FieldPath,
25 RegisterOptions,
26 FieldErrors,
27} from "react-hook-form";
form-provider.example.tsx
1import { FormField } from "@behivetech/forms.form-field";
2import { TextField } from "@behivetech/atoms.text-field";
3import { Button } from "@behivetech/atoms.button";
4import { Form, FormProvider, useForm } from "./index.js";
5
6interface DemoValues {
7 email: string;
8}
9
10export const BasicFormProvider = () => {
11 const methods = useForm<DemoValues>({ defaultValues: { email: "" } });
12 const { register, handleSubmit } = methods;
13
14 return (
15 <FormProvider methods={methods}>
16 <form onSubmit={handleSubmit((values) => console.log(values))}>
17 <FormField label="Email" htmlFor="email">
18 <TextField id="email" {...register("email")} />
19 </FormField>
20 </form>
21 </FormProvider>
22 );
23};
24
25export const BasicForm = () => {
26 const methods = useForm<DemoValues>({ defaultValues: { email: "" } });
27 const { register } = methods;
28
29 return (
30 <Form methods={methods} onSubmit={(values) => console.log(values)}>
31 <FormField label="Email" htmlFor="email-form">
32 <TextField id="email-form" {...register("email")} />
33 </FormField>
34 <Button type="submit" variant="primary" size="sm">
35 Save
36 </Button>
37 </Form>
38 );
39};