Published

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.

More like this