Skip to main content
161

Search Lumen

Find components, APIs, guides, and recipes.

GitHub
Web docs
React integration

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

bash
pnpm add @santi020k/lumen-react @santi020k/lumen-react-hook-form react-hook-form
pnpm add @santi020k/lumen-react @santi020k/lumen-react-hook-form react-hook-form

React 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

tsx
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.

AdapterValueLumen control
LumenSelectControllerstringSelect
LumenDatePickerControllerYYYY-MM-DD stringDatePicker
LumenInputOTPControllerstringInputOTP
LumenListBoxControllerstring or string[]ListBox
tsx
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.

bash
pnpm add @hookform/resolvers zod
tsx
import { 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))

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.
Web docsFull catalog