Glrk UI

Field with RHF

Reusable Field field wrappers built on top of shadcn/ui with react-hook-form

Installation

npx shadcn@latest add @glrk-ui/field-wrapper-rhf

If you haven't set up the prerequisites yet, check out Prerequest section.

Add following shadcn components: select, command, popover, calendar, textarea, input, radio-group, form, button, label, checkbox, switch.

Update same components from our site.

Copy and paste the following code into your project.

ui/field-wrapper-rhf.tsx
'use client'

import { Controller, Control, FieldValues, Path } from 'react-hook-form'

import { type multiSelectComboboxProps, type comboboxProps } from './combobox'
import { type selectProps } from './select'

import {
  InputWrapper as Input,
  TextareaWrapper as Textarea,
  RadioWrapper as Radio,
  CheckboxWrapper as Checkbox,
  SwitchWrapper as Switch,
  SelectWrapper as Select,
  DatePickerWrapper as DatePicker,
  ComboboxWrapper as Combobox,
  MultiSelectComboboxWrapper as MultiSelectCombobox,
} from './field-wrapper'

type BaseProps<T extends FieldValues> = {
  name: Path<T>
  control: Control<T>
  className?: string
  label?: React.ReactNode
}

type InputProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'value' | 'onChange' | 'onBlur'>
export function InputWrapper<T extends FieldValues>({ name, control, ...props }: InputProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <Input
          {...props}
          name={name}
          value={field.value ?? ''}
          onChange={field.onChange}
          onBlur={field.onBlur}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type TextareaProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'name' | 'value' | 'onChange' | 'onBlur'>
export function TextareaWrapper<T extends FieldValues>({ name, control, ...props }: TextareaProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <Textarea
          {...props}
          name={name}
          value={field.value ?? ''}
          onChange={field.onChange}
          onBlur={field.onBlur}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type RadioProps<T extends FieldValues> = BaseProps<T> & {
  options: (allowedPrimitiveT | optionT)[]
}
export function RadioWrapper<T extends FieldValues>({ name, control, ...props }: RadioProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <Radio
          {...props}
          name={name}
          value={field.value}
          onValueChange={field.onChange}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type CheckboxProps<T extends FieldValues> = BaseProps<T> & {
  options: (allowedPrimitiveT | optionT)[]
}
export function CheckboxWrapper<T extends FieldValues>({ name, control, ...props }: CheckboxProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <Checkbox
          {...props}
          name={name}
          value={field.value ?? []}
          onValueChange={field.onChange}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type SwitchProps<T extends FieldValues> = BaseProps<T>
export function SwitchWrapper<T extends FieldValues>({ name, control, ...props }: SwitchProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <Switch
          {...props}
          name={name}
          checked={field.value ?? false}
          onCheckedChange={field.onChange}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type SelectProps<T extends FieldValues> = BaseProps<T> & Omit<selectProps, 'value' | 'onValueChange'>
export function SelectWrapper<T extends FieldValues>({ name, control, ...props }: SelectProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <Select
          {...props}
          name={name}
          value={field.value}
          onValueChange={field.onChange}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type DatePickerProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.ComponentProps<typeof DatePicker>, 'name' | 'value' | 'onSelect' | 'error' | 'invalid'>
export function DatePickerWrapper<T extends FieldValues>({ name, control, ...props }: DatePickerProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <DatePicker
          {...props}
          name={name}
          value={field.value}
          onSelect={field.onChange}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type ComboboxProps<T extends FieldValues> = BaseProps<T> & Omit<comboboxProps, 'value' | 'onValueChange'>
export function ComboboxWrapper<T extends FieldValues>({ name, control, ...props }: ComboboxProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <Combobox
          {...props}
          name={name}
          value={field.value}
          onValueChange={field.onChange}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

type MultiSelectComboboxProps<T extends FieldValues> = BaseProps<T> & Omit<multiSelectComboboxProps, 'value' | 'onValueChange'>
export function MultiSelectComboboxWrapper<T extends FieldValues>({ name, control, ...props }: MultiSelectComboboxProps<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState }) => (
        <MultiSelectCombobox
          {...props}
          name={name}
          value={field.value ?? []}
          onValueChange={field.onChange}
          error={fieldState.error}
          invalid={fieldState.invalid}
        />
      )}
    />
  )
}

These wrappers automatically handle:

  • Label
  • Form control wiring
  • Error messages
  • Placeholder generation
  • Value parsing (e.g., converting "1" → number)

Usage

Basic

import { FormProvider, useForm } from "react-hook-form"

export function Basic() {
  const form = useForm({
    defaultValues: {/* ... */},
  })

  return (
    <FormProvider {...form}>
      <form
        onSubmit={form.handleSubmit((d) => console.log(d))}
      >
      // Wrapper component
      </form>
    </FormProvider>
  )
}

Controlled

import { FormProvider, useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"

const formSchema = z.object({
  // your schema
})

export function Controlled() {
  const form = useForm({
    resolver: zodResolver(formSchema),
    defaultValues: {/* ... */},
  })

  return (
    <FormProvider {...form}>
      <form
        onSubmit={form.handleSubmit((d) => console.log(d))}
      >
      // Wrapper component
      </form>
    </FormProvider>
  )
}

Reference

import { Control, FieldValues, Path } from "react-hook-form";

type BaseProps<T extends FieldValues> = {
  name: Path<T>
  label?: React.ReactNode
  control: Control<T>
  className?: string
}

InputWrapper

type InputProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'value' | 'onChange' | 'onBlur'>
<InputWrapper
  name="username"
  label="Username"
  control={form.control}
/>

TextareaWrapper

type TextareaProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'name' | 'value' | 'onChange' | 'onBlur'>
<TextareaWrapper
  name="bio"
  label="Bio"
  control={form.control}
/>

RadioWrapper

type RadioProps<T extends FieldValues> = BaseProps<T> & {
  options: (allowedPrimitiveT | optionT)[]
}
<RadioWrapper
  name="gender"
  label="Gender"
  control={form.control}
  options={["male", "female", "other"]}
/>

CheckboxWrapper

type CheckboxProps<T extends FieldValues> = BaseProps<T> & {
  options: (allowedPrimitiveT | optionT)[]
}
<CheckboxWrapper
  name="interest"
  label="Interest"
  control={form.control}
  options={["Book reading", "Music", "TV", "Movie"]}
/>

SwitchWrapper

type SwitchProps<T extends FieldValues> = BaseProps<T>
<SwitchWrapper
  name="isCompleted"
  label="Is completed"
  control={form.control}
/>

Note: Value need to be boolean.

SelectWrapper

type SelectProps<T extends FieldValues> =
  BaseProps<T> &
  Omit<selectProps, "value" | "onValueChange">
<SelectWrapper
  name="country"
  label="Country"
  control={form.control}
  options={[
    { value: "in", label: "India" },
    { value: "us", label: "USA" }
  ]}
/>

DatePickerWrapper

type DatePickerProps<T extends FieldValues> = BaseProps<T> &
  Omit<React.ComponentProps<typeof DatePicker>, 'name' | 'value' | 'onSelect' | 'error' | 'invalid'>
<DatePickerWrapper
  name="dob"
  label="Date of Birth"
  control={form.control}
/>

ComboboxWrapper

type ComboboxProps<T extends FieldValues> = BaseProps<T> &
  Omit<comboboxProps, "value" | "onValueChange">
<ComboboxWrapper
  name="fruit"
  label="Favorite Fruit"
  control={form.control}
  options={["Apple", "Banana", "Mango"]}
/>

MultiSelectComboboxWrapper

type MultiSelectComboboxProps<T extends FieldValues> = BaseProps<T> &
  Omit<multiSelectComboboxProps, "value" | "onValueChange">
<MultiSelectComboboxWrapper
  name="hobbies"
  label="Hobbies"
  control={form.control}
  options={["Music", "Sports", "Travel"]}
/>