How to handle array values in react-hook-form
react-hook-form is by far the most popular form library in React. Having used most of the big ones, I reckon react-hook-form may be the best possible set of compromises needed to implement forms in React.
Using react-hook-form, one of the things you'll run into sooner or later, is how to handle array values. Maybe it's a string[]
for email addresses or a number[]
for ids of some model. This is still a very simple to do, but if you're going to query Google with something like "react-hook-form array value", you will be pointed straight to useFieldArray
which would put you completely on the wrong track.
There's a much simpler solution, which is to use react-hook-form's Controller
component. With the Controller component, simple array field values are a breeze. A nice added bonus is that you also end up with an input component that is pure React and not entangled with react-hook-form at all.
The example
Imagine you have a list of articles that you want the user to select from. For each article you want to show details like the publication date and applicable tags to better inform the user. You decide you want to present this in a table with a checkbox for each article (and a checkbox in the table header to select all articles). This sounds like handling it in a form may get complicated, but with react-hook-form it's actually quite simple.
Let's start by fabricating some data;
const articles = [ { id: 1, title: 'Intro', publishedAt: '2024-04-01', tags: ['meta'], }, { id: 2, title: 'License', publishedAt: '2024-04-01', tags: ['meta'], }, { id: 3, title: 'The security implications of packages in front-end apps', publishedAt: '2024-04-15', tags: ['security', 'packages', 'npm', 'frontend'], },]
Now we can create a custom input component that allows a user to select articles. Since we don't need any special form logic, this can be a regular old React component;
import articles from './articles'
interface Props { value: number[] name: string onChange: (value: number[]) => void onBlur?: () => void disabled?: boolean}
const ArticleSelect = ({ value, name, onChange, onBlur, disabled }: Props) => ( <table> <thead> <tr> <th> <input type="checkbox" checked={value.length === articles.length} onClick={() => onChange( value.length === articles.length // Uncheck all ? [] // Check all : articles.map((article) => article.id), ) } /> </th> <th>Title</th> <th>Published</th> <th>Tags</th> </tr> </thead> <tbody> {articles.map((article) => { const checked = value.includes(article.id) const toggle = () => onChange( checked // Already in value: Remove from value ? value.filter((id) => id !== article.id) // Not yet in value: Add to value : [...value, article.id], )
return ( <tr key={article.id}> <td> <input type="checkbox" name={name} checked={checked} onChange={toggle} onBlur={onBlur} disabled={disabled} /> </td> <td>{article.title}</td> <td>{article.publishedAt}</td> <td>{article.tags.join(', ')}</td> </tr> ) })} </tbody> </table>)
export default ArticleSelect
It looks like a lot, but that's mostly just table markup. The important bits are the component's props, the articles.map
and the input elements.
The value
and onChange
props are the essentials parts of the custom input component. The value
prop is an array of the currently selected article ids and the onChange
prop is an event handler that will be called with the new value whenever the user selects or deselects an article.
The rest of the props are not strictly necessary; they are nice to have.
With articles.map
we loop over the articles to create a checkbox for each one. Each checkbox has an onChange handler that will toggle the article's id in the value array.
With this ArticleSelect
component ready to go, we can now use it in a react-hook-form form.
Using the Controller
component, we can use our custom input in our form. The Controller
component will handle all the plumbing needed to keep the form state in sync with the input. Since our ArticleSelect
input handles all the standard field props such as value
, onChange
and onBlur
, we can simply pass the entire field
prop to ArticleSelect
.
<Controller control={control} name="articles" render={({ field }) => <ArticleSelect {...field} />}/>
And that's it! Not only is the code in our form very simple, we also have a very clean and simple input component that can be reused anywhere in our application. The ArticleSelect
component is completely decoupled from react-hook-form and could easily be used in any other form library or even without a form library at all.
This is a great example of how react-hook-form allows us to write standard React components that are completely decoupled from the form library.
More complex types
You can even use this for more complicated types such as an object of objects indexed by id such as:
type ArticlesById = { [articleId: number]: { publishedAt: Date updatedAt: Date }}
Simply map over the articles as in our previous example, use a Controller
for each value, and set the Controller
's names to `articles.${articleId}.publishedAt`
and `articles.${articleId}.updatedAt`
respectively.
You really rarely actually need useFieldArray
.