Max Zavatyi

React Hook Form with Custom Components

Dec 9, 2024

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.

title

It’s integrated with React Hook Form via Controller and includes validation. To proceed, at least one day must be selected.

title

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.

Share on:

Found this article helpful? Consider buying me a coffee and sharing your thoughts – I'd love to hear from you!