Using React Hook Form with Controller? No more

Enyel Sequeira
4 min readMar 9, 2023

--

home page RHF

Introduction

React Hook Form is a popular library that simplifies the process of building forms in React. One of its key features is the use of “Controller” components, which provides a way to connect an external UI component such for example a component from Material Mantine or Chakra ui.

I absolutely love RHF and have made my job easier, however one thing that I really dislike is how sometimes it can be hard to make it work with other components libraries (or maybe its just me).

I always struggle specially setting up headless ui tailwind and the main reason, is the way internals of the library behave. I will show a way to make it work nice and easily without any hassle or any bugs (at least I haven't found any yet) similar I will show you how you can remove the usage of Controller and use the hook with other component libraries

RHF definition of the hook is here https://react-hook-form.com/api/usecontroller in short this hook allow us to do everything the Controllercomponent does

Let’s look at a simple code

let’s see how the Controller works in case you are not aware of it

const {test} = useForm<YOURTYPE>()
<Controller
control={test.control}
name="endDate"
render={({ field: { onChange } }) => (
<DatePicker
defaultValue={new Date()}
classNames={{
root: "w-72",
}}
onChange={(e) => onChange(e?.toString())}
maxDate={new Date()}
allowFreeInput
label="To"
placeholder="To"
icon={<IconCalendar size={16} />}
/>
)}
/>;

now the code above works just fine, this is the implementation if you are using mantine this is the component library we are using, what this is doing, is just passing the onChange that we get from the Controller to the DatePicker and as expected we would get a value in string format

Imagine you will have to pick a date in different parts of your code sure you can keep adding a controllereach time and yes it will work, instead what I propose is to use this as a starting point and make it better using TSand default props we get from both RHFand mantine let's see an alternative

import type { FieldValues,  UseControllerProps} from "react-hook-form";
import { useController} from "react-hook-form";
export type OwnDatePickerProps<T extends FieldValues> =
UseControllerProps<T> & Omit<DatePickerProps, "value" | "defaultValue">;
export function OwnDatePicker<T extends FieldValues>({
name,
control,
defaultValue,
rules,
shouldUnregister,
onChange,
...props
}: OwnDatePickerProps<T>) {
const {
field: { value, onChange: fieldOnChange, ...field },
fieldState,
} = useController<T>({
name,
control,
defaultValue,
rules,
shouldUnregister,
});
return (
<DatePicker
defaultValue={new Date()}
value={value}
onChange={(e) => {
fieldOnChange(e?.toString());
}}
error={fieldState.error?.message}
{...field}
{...props}
/>
);
}

we create our own type using TSand omit certain values that we don't need or we don't want to control, and just let RHF do its thing, we use generics to achieve this, I am not a TS wizard, but this approach allow us to use the component without Controller like so

const {test} = useForm<YOURTYPE>()
<>
....code
<OwnDatePicker
name="endDate"
control={test.control}
maxDate={new Date()}
allowFreeInput
label="To"
placeholder="To"
icon={<IconCalendar size={16} />}
classNames={{
root: "w-72",
}}
/>
.....
</>

and the beauty of this is that it is typesafe and if one props is not there TSwill start complaining, avoiding errors

now let’s look at another of my favorite which is headless ui I have a love/hate relationship when setting it up with RHF not anymore though found a good way to make it work, overwriting/omiting and using the RHF default to handle change state. Let's look at some code

import { Listbox, Transition, ListboxProps } from "@headlessui/react";
import {
FieldValues,
useController,
UseControllerProps,
} from "react-hook-form";
export type OwnMultiSelectProps<
T extends FieldValues,
TTag extends ElementType = "div",
TType = string,
TActualType extends {} = Record<string, any>
> = UseControllerProps<T> &
Omit<ListboxProps<TTag, TType, TActualType>, "value" | "defaultValue"> & {
// OWN PROPS YOU WOULD LIKE
};
// usageexport function OwnMultiSelect<
T extends FieldValues,
TTag extends ElementType = "div",
TType = string,
TActualType extends {} = Record<string, any>
>({
name,
control,
defaultValue,
rules,
shouldUnregister,
...props
}: OwnMultiSelectProps<T, TTag, TType, TActualType>) {
const {
field: { value, onChange: fieldOnChange, ...field },
fieldState,
} = useController<T>({
name,
control,
defaultValue,
rules,
shouldUnregister,
});

return (
<div className="w-72">
<Listbox
value={selectedItems}
onChange={(e) => fieldOnChange(e)}
multiple
>
<div className="relative mt-1">
// your code here
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
// your code here
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
);
}

and with this, you avoid workaround, like the ones I have found online, in here you allow RHF to handle most of the work, regarding the TS like I said I am not an expert, but this seems to work fine, without any complaints and you still get the goodies from Listbox props

Conclusion

In conclusion, while React Hook Form is a great library for building forms in React, it can be challenging to use with other component libraries. The Controller component can help, but it can also be cumbersome to use repeatedly throughout your code. By creating your own components that utilize the useController hook and generic types, you can simplify the process and make it more typesafe. By doing so, you can take advantage of the features of other component libraries while still benefiting from the ease of use of React Hook Form.

--

--

Enyel Sequeira

Just a friendly developer who enjoys coding things from scratch