Using with React Hook Form
Connect StackForm to React Hook Form via RHFFormProvider
Install
npm install @stackform/core @stackform/ui @stackform/rhf react-hook-formQuick start
A complete contact form with two fields, error display, and submission:
import { useForm } from 'react-hook-form'
import { StackFormProvider, TextField } from '@stackform/ui'
import { RHFFormProvider } from '@stackform/rhf'
interface FormValues {
name: string
email: string
}
export function ContactForm() {
const form = useForm<FormValues>({
defaultValues: { name: '', email: '' },
})
return (
<StackFormProvider>
<RHFFormProvider form={form}>
<form onSubmit={form.handleSubmit(console.log)} className="space-y-4">
<TextField name="name" label="Name" placeholder="Your name" />
<TextField
name="email"
label="Email"
type="email"
placeholder="you@example.com"
hint="We'll never share your email."
/>
<button type="submit">Submit</button>
</form>
</RHFFormProvider>
</StackFormProvider>
)
}Provider setup
Two providers are required, in this order:
| Provider | Package | Purpose |
|---|---|---|
StackFormProvider | @stackform/ui | Provides default slot implementations and shared classNames |
RHFFormProvider | @stackform/rhf | Bridges React Hook Form's form instance into StackForm context |
StackFormProvider must be the outer wrapper. Field components call useField() internally — you never wire them to the form instance manually.
Error display
Errors surface automatically. When React Hook Form sets a field error — via setError, a resolver, or built-in validation — the field reads it from context and renders the message below the input. No prop wiring required.
const form = useForm<{ email: string }>({
defaultValues: { email: '' },
})
// Trigger an error manually
form.setError('email', { message: 'Invalid email address' })
// TextField renders it automatically
<TextField name="email" label="Email" />To trigger errors on submit, use a resolver (e.g. @hookform/resolvers/zod) or React Hook Form's built-in validation rules. StackForm displays whatever the form library sets.
Form submission
Call form.handleSubmit on the <form> element's onSubmit. React Hook Form validates all fields before calling your handler — if validation fails, errors appear in the fields automatically.
const form = useForm<FormValues>({
defaultValues: { name: '', email: '' },
})
async function onSubmit(values: FormValues) {
await saveToApi(values)
}
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* fields */}
<button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Saving…' : 'Save'}
</button>
</form>useRHFForm factory
useRHFForm is an optional factory that returns both the form handle and a pre-wired FormProvider component, reducing import surface and improving TypeScript inference:
import { useRHFForm } from '@stackform/rhf'
import { StackFormProvider, TextField } from '@stackform/ui'
interface FormValues {
name: string
email: string
}
export function ContactForm() {
const { form, FormProvider } = useRHFForm<FormValues>({
defaultValues: { name: '', email: '' },
})
return (
<StackFormProvider>
<FormProvider>
<form onSubmit={form.handleSubmit(console.log)} className="space-y-4">
<TextField name="name" label="Name" />
<TextField name="email" label="Email" type="email" />
<button type="submit">Submit</button>
</form>
</FormProvider>
</StackFormProvider>
)
}FormProvider is memoised inside the hook so it does not re-create on every render.
Slot override example
Replace any slot with a custom component. Slot components receive only display-level props and are unaware of the form library.
import type { LabelSlotProps } from '@stackform/ui'
function BoldLabel({ htmlFor, children, required }: LabelSlotProps) {
return (
<label htmlFor={htmlFor} className="font-bold uppercase tracking-wide text-xs">
{children}
{required ? <span aria-hidden="true"> *</span> : null}
</label>
)
}
<TextField
name="email"
label="Email"
slots={{ Label: BoldLabel }}
/>