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:
-
value
- It should show the current value of the field. -
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} />
)}
/>
</>
)
Note: if you're using React Hook Form V6 or earlier, the function signature here is slightly different.
value
andonChange
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>
);
}
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>
);
}
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:
- Receive and render the current value/state of the field, commonly through the
value
prop. - Call a function when that value should be updated, commonly through the
onChange
prop.
Top comments (10)
Thank you for writing this blog post!
Thank you for your work, Bill!
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} />}
Someone please share how to post multipart/form-data using React-Form-Hook. by default it generate Content-Type: application/JSON.
Thank you, I'm a little confused on how to fix some bugs in an app and the article helped me
great article, thanks
Life Saving article bro. Just the thing I wanted to perform.
Please give example with WYSWYG html editior such as draf.js
Very well written!
Estava com problema ao iniciar DatePicker do MUI com valor default e esse post me ajudou.
Obrigado!