React Hook Form with Custom Components
I’ve noticed that many people wonder how to connect more complex components to React Hook Form, beyond the basic input fields. This article will guide you through the process and make it straightforward!
I'm assuming you're already familiar with the basics of React Hook Form, as this article focuses on how to connect various custom components, such as select elements, checkboxes with multiple selections, and custom inputs. While I could include more examples, the principle remains the same for every type of component. In this article, I will demonstrate examples using Radix UI and React Select to quickly create ready-to-use components that adhere to best practices. However, the approach I'll show can also be applied to purely custom components.
So let's start with example by defining our form hook. This is a fully typed example, and we are using the Zod library for validation.
const formMethods = useForm<FormFieldsType>({
resolver: zodResolver(formFieldsSchema),
mode: 'onBlur',
defaultValues: {
gender: '',
weeklySchedule: [],
birthDate: '',
},
});
Select
Let's start with a simple and common example of how to use React Select with React Hook Form:
<Controller
name="gender"
control={control}
render={({ field }) => (
<Select
options={options}
placeholder="Select"
defaultValue={field.value}
onChange={(selectedItem) =>
field.onChange(selectedItem?.value)
}
/>
)}
/>
To work with custom components, we use the Controller wrapper, which simplifies their integration.
control
: The object returned from invoking useForm. This is optional when using FormProvider.name
: A unique name for your input.render
: A function that returns the field object, which contains properties provided by the Controller.field.value
: The current value of the controlled component.field.onChange
: A function that updates the library with the value.
The key pattern here is that we wrap the component in a Controller
and pass an onChange
prop, which is a callback function. Inside that callback, we access the selected item's value and pass it to Hook Form via the field.onChange
method. Learn more in the official docs: Controller.
Multiple checkboxes
Now for a more complex example: Here, we have a component that functions as multiple checkboxes styled as select cards, allowing users to choose different days of the week. This example is inspired by a real fitness project I worked on called GoChamp.
It’s integrated with React Hook Form via Controller
and includes validation. To proceed, at least one day must be selected.
Here’s a full code example. It may look extensive, but that’s only because we use a map and filter here. I didn’t want to abstract that away since showing the full process makes it easier to understand how everything connects - which is actually very simple.
<Controller
control={control}
name="weeklySchedule"
render={({ field }) => (
<CheckboxCardsGroup
items={DAYS_OF_WEEK.map((label) => ({
label,
checked: field.value ? field.value.includes(label) : false,
}))}
checkedItems={field.value || []}
handleCheckboxChange={(label) => {
if (field.value) {
const newCheckedItems = field.value.includes(label)
? field.value.filter((item) => item !== label)
: [...field.value, label];
field.onChange(newCheckedItems);
setUserAnswersSessionData("weeklySchedule", newCheckedItems);
}
if (scheduleError) {
clearErrors("weeklySchedule");
}
}}
/>
)}
/>;
type Props = {
checkedItems: string[];
items: { label: string; checked: boolean }[];
handleCheckboxChange: (label: string) => void;
};
const CheckboxCardsGroup: FC<Props> = ({
items,
checkedItems,
handleCheckboxChange,
}) => {
return (
<>
{items.map((item, index) => (
<label
key={index}
className={`${s.labelRoot} ${
checkedItems.includes(item.label) ? s.active : ""
}`}
htmlFor={item.label}
>
<RadixCheckbox.Root
className={s.checkboxRoot}
checked={checkedItems.includes(item.label)}
onCheckedChange={() => handleCheckboxChange(item.label)}
id={item.label}
>
<RadixCheckbox.Indicator>
<CheckmarkIcon />
</RadixCheckbox.Indicator>
</RadixCheckbox.Root>
{item.label}
</label>
))}
</>
);
};
Do you see the pattern here? Just like in the previous example with the select component, we wrap the checkboxes in a Controller
. The key is to pass the checked value to Hook Form using field.onChange(newCheckedItems)
. This demonstrates why passing a callback function as a prop is so powerful - it allows us to validate the value or implement custom logic before sending it to Hook Form.
Additionally, we can clear the error when selecting a new value, like this:
if (scheduleError) {
clearErrors("weeklySchedule");
}
Date picker
Here’s another example with a date picker. Think this one can be difficult? Of course not! It works just like the previous examples.
<Controller
name='birthDate'
control={control}
render={({ field }) => (
<DatePicker
value={field.value}
label='Birth date'
onClick={(value) => field.onChange(value)}
errorMessage={errors.birthDate?.message}
/>
)}
/>
Just like with other components, we wrap the date picker in a Controller
to manage it. Whenever a date is selected, we pass the value to React Hook Form using the field.onChange
method.
And that’s basically it! Of course, there’s always more you can do, but the basics of connecting any component are as simple as this.