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-rhfIf 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.
'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"]}
/>