DEV Community

Cover image for Turn Anything Into A Form Field With React Hook Form Controller
Erik Lyngved
Erik Lyngved

Posted on • Edited on

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 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 */}
  />
);
Enter fullscreen mode Exit fullscreen mode

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)}
  />
)
Enter fullscreen mode Exit fullscreen mode

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: { value, onChange }}) => (
        <input value={value} onChange={onChange} />
      )}
    />
  </>
)
Enter fullscreen mode Exit fullscreen mode

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

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, the fields will use the same value and onChange prop names. If this is the case, we can simply spread the {...field} object into the component.

Other times, 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.

export default function App() {
  const { control, handleSubmit } = useForm({
    defaultValues: {
      textField: "",
      checkbox: false
    }
  });

  const onSubmit = (values) => alert(JSON.stringify(values));

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        control={control}
        name="textField"
        render={({ field }) => (
          // Material UI TextField already supports
          // `value` and `onChange`
          <TextField {...field} label="Text field" />
        )}
      />

      <Controller
        control={control}
        name="checkbox"
        render={({ field: { value, onChange } }) => (
          // Checkbox accepts its value as `checked`
          // so we need to connect the props here
          <FormControlLabel
            control={<Checkbox checked={value} onChange={onChange} />}
            label="I am a checkbox"
          />
        )}
      />

      <Button type="submit" variant="contained" color="primary">
        Submit
      </Button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Check out the full example on Code Sandbox

Building from scratch: a five star rating field

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.

// StarButton displays a single star
// It is controlled via active and onClick props
const StarButton = ({ active, onClick }) => (
  <button type="button" onClick={onClick}>
    {active ? <Star color="secondary" /> : <StarBorder />}
  </button>
);

// StarField uses 5 StarButtons to create a field
// with value and onChange props
const StarField = ({ value, onChange }) => (
  <>
    <StarButton active={value >= 1} onClick={() => onChange(1)} />
    <StarButton active={value >= 2} onClick={() => onChange(2)} />
    <StarButton active={value >= 3} onClick={() => onChange(3)} />
    <StarButton active={value >= 4} onClick={() => onChange(4)} />
    <StarButton active={value >= 5} onClick={() => onChange(5)} />
  </>
);

export default function App() {
  const { control, handleSubmit } = useForm({
    defaultValues: {
      rating: 0
    }
  });

  const onSubmit = ({ rating }) => {
    alert(`Your rating: ${rating}`);
  };

  return (
    <Container>
      <form onSubmit={handleSubmit(onSubmit)}>
        <Controller
          control={control}
          name="rating"
          render={({ field }) => <StarField {...field} />}
        />

        <Button type="submit">Submit</Button>
      </form>
    </Container>
  );
}
Enter fullscreen mode Exit fullscreen mode

Check out the full example on Code Sandbox

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.

Top comments (10)

Collapse
 
bluebill1049 profile image
Bill

Thank you for writing this blog post!

Collapse
 
elyngved profile image
Erik Lyngved

Thank you for your work, Bill!

Collapse
 
the_previ profile image
Luca • Edited

Great article, thanks!
Just a minor thing: in the example of controlled custom component to avoid displaying the warning "Function components cannot be given refs" just avoid to give "ref" prop to the component, like this:

render={({ field: { onChange, value } }) => <StarField onChange={onChange} value={value} />}

Collapse
 
devopshasan profile image
Hasan Habib

Someone please share how to post multipart/form-data using React-Form-Hook. by default it generate Content-Type: application/JSON.

Collapse
 
thiagotfsilva profile image
thiagotfsilva

Thank you, I'm a little confused on how to fix some bugs in an app and the article helped me

Collapse
 
yaldakarimi profile image
Yalda Karimi

great article, thanks

Collapse
 
theswordbreaker profile image
The Sword Breaker

Life Saving article bro. Just the thing I wanted to perform.

Collapse
 
devopshasan profile image
Hasan Habib

Please give example with WYSWYG html editior such as draf.js

Collapse
 
ravgeetdhillon profile image
Ravgeet Dhillon

Very well written!

Collapse
 
diegomoreto profile image
DiegoMoreto • Edited

Estava com problema ao iniciar DatePicker do MUI com valor default e esse post me ajudou.
Obrigado!