Lumen with React Hook Form
Register native-backed controls directly. Add the optional adapter package only for composite value contracts.
- Native controls
- 10
- Adapters
- 4
- RHF
- Optional
Install
pnpm add @santi020k/lumen-react @santi020k/lumen-react-hook-form react-hook-formpnpm add @santi020k/lumen-react @santi020k/lumen-react-hook-form react-hook-formReact Hook Form remains a peer dependency of the adapter package and is not pulled into applications that use Lumen’s native form mode.
Register native controls
The ref reaches the submitted native element, and name, blur, change, required, disabled, and focus management pass through unchanged.
Input, Textarea, Checkbox, Switch, Slider, NativeSelect, NumberField, SearchField, TimeField, FileUpload
import {
Button,
Field,
FieldError,
Form,
Input,
Label
} from '@santi020k/lumen-react'
import { useForm } from 'react-hook-form'
type ProfileValues = { email: string }
export function ProfileForm() {
const {
formState: { errors, isSubmitting },
handleSubmit,
register
} = useForm<ProfileValues>()
return (
<Form
onSubmit={handleSubmit(saveProfile)}
status={isSubmitting ? 'submitting' : errors.email ? 'error' : 'idle'}
>
<Field invalid={Boolean(errors.email)}>
<Label htmlFor="email">Email</Label>
<Input
aria-describedby={errors.email ? 'email-error' : undefined}
aria-invalid={Boolean(errors.email)}
id="email"
type="email"
{...register('email', { required: 'Email is required' })}
/>
<FieldError id="email-error" message={errors.email?.message} />
</Field>
<Button disabled={isSubmitting} type="submit">Save</Button>
</Form>
)
}import {
Button,
Field,
FieldError,
Form,
Input,
Label
} from '@santi020k/lumen-react'
import { useForm } from 'react-hook-form'
type ProfileValues = { email: string }
export function ProfileForm() {
const {
formState: { errors, isSubmitting },
handleSubmit,
register
} = useForm<ProfileValues>()
return (
<Form
onSubmit={handleSubmit(saveProfile)}
status={isSubmitting ? 'submitting' : errors.email ? 'error' : 'idle'}
>
<Field invalid={Boolean(errors.email)}>
<Label htmlFor="email">Email</Label>
<Input
aria-describedby={errors.email ? 'email-error' : undefined}
aria-invalid={Boolean(errors.email)}
id="email"
type="email"
{...register('email', { required: 'Email is required' })}
/>
<FieldError id="email-error" message={errors.email?.message} />
</Field>
<Button disabled={isSubmitting} type="submit">Save</Button>
</Form>
)
}Use adapters for composites
These components use useController internally and translate value, blur, disabled state, and the focus ref without hiding React Hook Form’s rules or form state.
| Adapter | Value | Lumen control |
|---|---|---|
| LumenSelectController | string | Select |
| LumenDatePickerController | YYYY-MM-DD string | DatePicker |
| LumenInputOTPController | string | InputOTP |
| LumenListBoxController | string or string[] | ListBox |
import { Field, FieldError, Label } from '@santi020k/lumen-react'
import {
getLumenManagedFieldState,
LumenSelectController
} from '@santi020k/lumen-react-hook-form'
import { useForm } from 'react-hook-form'
type Values = { role: string }
const { control, formState: { errors } } = useForm<Values>({
defaultValues: { role: '' }
})
const state = getLumenManagedFieldState({
error: errors.role,
invalid: Boolean(errors.role)
}, 'role')
<Field invalid={state.invalid}>
<Label htmlFor={state.controlId}>Role</Label>
<LumenSelectController
aria-describedby={state['aria-describedby']}
aria-invalid={state['aria-invalid']}
control={control}
id={state.controlId}
name="role"
options={[
{ label: 'Designer', value: 'designer' },
{ label: 'Engineer', value: 'engineer' }
]}
placeholder="Choose a role"
rules={{ required: 'Choose a role' }}
/>
<FieldError id={state.errorId} message={state.errorMessage} />
</Field>import { Field, FieldError, Label } from '@santi020k/lumen-react'
import {
getLumenManagedFieldState,
LumenSelectController
} from '@santi020k/lumen-react-hook-form'
import { useForm } from 'react-hook-form'
type Values = { role: string }
const { control, formState: { errors } } = useForm<Values>({
defaultValues: { role: '' }
})
const state = getLumenManagedFieldState({
error: errors.role,
invalid: Boolean(errors.role)
}, 'role')
<Field invalid={state.invalid}>
<Label htmlFor={state.controlId}>Role</Label>
<LumenSelectController
aria-describedby={state['aria-describedby']}
aria-invalid={state['aria-invalid']}
control={control}
id={state.controlId}
name="role"
options={[
{ label: 'Designer', value: 'designer' },
{ label: 'Engineer', value: 'engineer' }
]}
placeholder="Choose a role"
rules={{ required: 'Choose a role' }}
/>
<FieldError id={state.errorId} message={state.errorMessage} />
</Field>Validate with Zod or Yup
Install the official React Hook Form resolvers with one schema library. The resolver owns schema parsing and returns standard React Hook Form errors, so native controls and Lumen’s composite adapters use the same field-state mapping shown above.
pnpm add @hookform/resolvers zodpnpm add @hookform/resolvers yupimport { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
const profileSchema = z.object({
email: z.string().trim().email('Enter a valid email'),
role: z.string().trim().min(1, 'Choose a role')
})
type ProfileInput = z.input<typeof profileSchema>
type ProfileValues = z.output<typeof profileSchema>
const {
control,
formState: { errors },
handleSubmit,
register
} = useForm<ProfileInput, unknown, ProfileValues>({
defaultValues: { email: '', role: '' },
resolver: zodResolver(profileSchema)
})
// Use register('email') on Input and pass control to
// LumenSelectController. Render errors through FieldError.
const submit = handleSubmit(async values => saveProfile(values))import { yupResolver } from '@hookform/resolvers/yup'
import { useForm } from 'react-hook-form'
import * as yup from 'yup'
const profileSchema = yup.object({
email: yup.string().trim().email('Enter a valid email').required('Email is required'),
role: yup.string().trim().required('Choose a role')
})
type ProfileValues = yup.InferType<typeof profileSchema>
const {
control,
formState: { errors },
handleSubmit,
register
} = useForm<ProfileValues>({
defaultValues: { email: '', role: '' },
resolver: yupResolver(profileSchema)
})
// Use register('email') on Input and pass control to
// LumenSelectController. Render errors through FieldError.
const submit = handleSubmit(async values => saveProfile(values))Zod and Yup remain application dependencies. Lumen does not bundle either library and does not alter resolver output, schema transforms, or server-validation behavior.
Integration rules
- Put initial values in useForm defaultValues so controlled fields never switch modes.
- Let React Hook Form own validation; do not add useFormValidation to the same form.
- Use Zod or Yup through @hookform/resolvers when the application needs schema validation.
- Keep authorization, server errors, and persistence in the application.
- Map server field errors with setError, then focus the ErrorSummary or first invalid field.