Turn Anything Into A Form Field With React Hook Form Controller

Cover image photo by Chris J. Davis on Unsplash

React Hook Form has quickly become my favorite library to wrangle forms of all shapes and sizes, mainly for its great developer experience. The 30 second screencast on their home page nicely illustrates …

Cover image photo by Chris J. Davis on Unsplash

React Hook Form has quickly become my favorite library to wrangle forms of all shapes and sizes, mainly for its great developer experience. The 30 second screencast on their home page nicely illustrates how to integrate it into a standard form using the magic of register to connect each field. When using native <input/> components, it's pretty simple to get up and running.

But in the real world, we often don't work with vanilla inputs. Popular UI libraries often abstract and wrap any underlying form elements, making it hard or impossible to use with register.

Sometimes we want to delight our users with a custom interactive component, like rating a product with 5 actual star icons instead of a boring select box. How can we connect these to an existing form without messy logic?

Enter the Controller

The library exports a <Controller/> component which was made for exactly this purpose. It allows us to connect any component to our form, enabling it to display and set its value.

To use it, you'll need the control object returned from useForm() instead of register. Also, as usual, you'll need a name to tell the form which field we are controlling. Finally, the render prop is where we place our component.

// Controller syntax

const { control } = useForm();

return (
  <Controller
    control={control}
    name="myField"
    render={/* Custom field component goes here */}
  />
);

Making the Field Component

Why is it called Controller? It could be because our field component needs to be a controlled component.

In a nutshell, a controlled component is one that gets and sets its current "state" via props. In the case of a form field, that state is the field's current value.

<input/> is one example of a component that can be controlled. We tell the input what its current value is, and we give it a way to tell us when that value should be changed.

// <input/> as a controlled component in a standard React form

const [val, setVal] = useState('')

return (
  <input
    type="text"
    value={val}
    onChange={e => setVal(e.target.value)}
  />
)

Here we see the two props required to make our field component work with the Controller:

  1. value - It should show the current value of the field.
  2. onChange - It should be able to tell the Controller when a change to the current value is made.

These also happen to be two of the properties handed to us by the render function! Its signature includes a field object which has value and onChange (among other things).

It doesn't make much sense to use the Controller for a basic input, but here it is for illustration purposes:

// Using a basic input in a Controller
// (though you can just use `register` here)

const { control } = useForm();

return (
  <>
    <Controller
      control={control}
      name="myField"
      render={({ field }) => (
        <input {...field} />
        // Equivalent to <input value={field.value} onChange={field.onChange} />
      )}
    />
  </>
)

Note: if you're using React Hook Form V6 or earlier, the function signature here is slightly different. value and onChange are instead top-level properties of the argument, looking like the following instead.

// V6 or earlier
render=({ value, onChange }) => (
  <input value={value} onChange={onChange}  />
)

Real Examples

Using a UI library: Material UI

Check out the full example on Code Sandbox

Many projects use form inputs from popular UI libraries like Material UI. The problem is that any <input/> components are usually hidden from us, so we can't use register to connect them to our form. This is where Controller comes in!

Often they will use the same value and onChange props we're used to seeing.

If this is the case, we can simply spread the {...field} object into the component.

// Using a Material-UI TextField component

<Controller
  control={control}
  name="myTextField"
  render={({ field }) => <TextField {...field} />}
/>

Sometimes the props are not named the same. For example, Checkbox accepts its value as checked instead of value. This means we can't easily spread field into it, but the result is still fairly easy to put together.

// Using a Material-UI Checkbox component

<Controller
  control={control}
  name="myCheckbox"
  render={({ field: { value, onChange }}) => (
    <Checkbox checked={value} onChange={onChange} />
  )}
/>

Building from scratch: a five star rating field

Check out the full example on Code Sandbox

We've all probably used the ubiquitous widget that allows us to rate anything by clicking on a row of star icons. Thankfully, if we are just able to create a controlled component, we can cleanly fit it into the rest of the form.

Let's pretend we have a simple StarIcon component that renders a single star icon. It accepts a active boolean prop that tells it whether the icon should be filled in or not, and it can be clicked to fire an onClick handler. If we use the value and onChange props as provided by the Controller's field, we can construct a line of stars like so:

// Our controlled five star field component

const FiveStarField = ({ value, onChange }) => (
  <>
    <StarIcon active={value >= 1} onClick={() => onChange(1)} />
    <StarIcon active={value >= 2} onClick={() => onChange(2)} />
    <StarIcon active={value >= 3} onClick={() => onChange(3)} />
    <StarIcon active={value >= 4} onClick={() => onChange(4)} />
    <StarIcon active={value >= 5} onClick={() => onChange(5)} />
  </>
)

Since we're using the usual props, we can simply pass this into the Controller with render={({ field }) => <FiveStarField {...field} />.

Conclusion

Using <Controller/> and a properly controlled component, you can make pretty much anything into a form field compatible with React Hook Form. The field can be as simple or fancy as you want, with any logic encapsulated in it, as long as it does these two things:

  1. Receive and render the current value/state of the field, commonly through the value prop.
  2. Call a function when that value should be updated, commonly through the onChange prop.

Print Share Comment Cite Upload Translate
CITATION GOES HERE CITATION GOES HERE
Select a language: