Creating a Group of MUI Checkboxes Controlled by react-hook-form

Tech

  • MUI 5.15.19
  • react-hook-form 7.51.4

Checkbox group

React-hook-form prefers uncontrolled components, which clashes with many component libraries (such as MUI or AntDesign). Thankfully it provides a convenient wrapper in the form of <Controller /> component. That works smoothly in most cases, however I've ran into problems with a group of multiple checkboxes. My wish was to have just one field, that would be represented by an array of checked values, as that is the format used by react-hook-form.

So in the end form data with checked boxes with values "answer-1" and "answer-3" should look like this:

1{"usersAnswers": ["answer-1", "answer-3"]}

A lot of the solutions I've found online didn't work. I suspect they were written for MUI v4 and/or react-hook-form v6... Not completely sure which package was the culprit but in the end the simplest solution seems to be our own state handling of checked boxes. Something like this:

1import { Checkbox, FormControl, FormControlLabel, FormGroup, Typography } from "@mui/material";
2import { useController, useWatch } from "react-hook-form";
3
4export const CheckboxGroup = ({
5    control,
6    name,
7    checkboxes,
8}: { control: any, name: string, checkboxes: { value: string, label: string }[] }) => {
9    const {
10        field: { ref, value, onChange, ...inputProps },
11        formState: { errors },
12    } = useController({
13        name,
14        control,
15        defaultValue: [],
16    })
17    const checkboxIds = useWatch({ control, name: name }) || []
18
19    function handleChange(value: any) {
20        const newArray = [...checkboxIds]
21        const item = value
22
23        if (newArray.length > 0) {
24            const index = newArray.findIndex((x) => x === item)
25            if (index === -1) {
26                newArray.push(item)
27            } else {
28                newArray.splice(index, 1)
29            }
30        } else {
31            newArray.push(item)
32        }
33        onChange(newArray)
34    }
35
36    return (
37        <div>
38            <FormControl>
39                <FormGroup>
40                    {checkboxes.map((option: any) => (
41                        <FormControlLabel
42                            control={
43                                <Checkbox
44                                    checked={value?.some(
45                                        (checked: any) => checked === option.value
46                                    )}
47                                    {...inputProps}
48                                    inputRef={ref}
49                                    onChange={() => handleChange(option.value)}
50                                />
51                            }
52                            label={<Typography component="span">{option.label}</Typography>}
53                            key={option.value}
54                        />
55                    ))}
56                </FormGroup>
57            </FormControl>
58        </div>
59    )
60}
61            

Usage is simple. Pass the form control from react-hook-form (or access it directly in the CheckboxGroup using the useFormContext hook.)

1// pass the control to our CheckboxGroup from useForm hook from react-hook-form
2export default function PageWithForm() {
3    const { control } = useForm()
4    const checkboxes = [
5        { value: "answer-1", label: "First answer", },
6        { value: "answer-2", label: "Second answer", },
7        { value: "answer-3", label: "Third answer", },
8    ]
9
10    return <CheckboxGroup
11        control={control}
12        checkboxes={checkboxes}
13        name="usersAnswers"
14    />
15}
16