Piplup
@piplup/rhf-adaptersmui-materialHooks

useMuiCheckboxAdapter

Returns props that connect an MUI Checkbox to React Hook Form, storing checked values in an array, with custom checked and indeterminate logic.

useMuiCheckboxAdapter connects one MUI Checkbox to a React Hook Form field and returns props you spread onto it. It's what MuiCheckboxElement uses internally.

Use the element for plain checkbox lists. Reach for the hook when one component should own a whole group (label, options, error text), or for checkboxes whose state isn't simply "is my value in the array", such as a "select all" box that controls the rest.

Import

import { useMuiCheckboxAdapter } from "@piplup/rhf-adapters/mui-material";

Usage

The "All channels" box is checked when every channel is selected, indeterminate when some are, and selects or clears all of them on click. Uncheck everything and submit to see the required message, which the same hook provides as helperText.

Notify me by
"use client";import Checkbox from "@mui/material/Checkbox";import FormControl from "@mui/material/FormControl";import FormControlLabel from "@mui/material/FormControlLabel";import FormGroup from "@mui/material/FormGroup";import FormHelperText from "@mui/material/FormHelperText";import FormLabel from "@mui/material/FormLabel";import Stack from "@mui/material/Stack";import {  MuiButtonElement,  useMuiCheckboxAdapter,} from "@piplup/rhf-adapters/mui-material";import type * as React from "react";import { type Control, useForm } from "react-hook-form";type Channel = "email" | "sms" | "push";type FormValues = { channels?: Channel[] };const CHANNELS: Array<{ value: Channel; label: string }> = [  { value: "email", label: "Email" },  { value: "sms", label: "SMS" },  { value: "push", label: "Push notifications" },];// Every checkbox registers the same field, so they share these rules.const sharedProps = {  name: "channels",  required: true,  messages: { required: "Pick at least one channel" },} as const;// One option. Checking it adds `value` to the array, unchecking removes it.function ChannelCheckbox({  control,  value,  label,}: {  control: Control<FormValues>;  value: Channel;  label: string;}) {  // Checkbox has no error or helperText props, so keep them off the DOM.  // `required` is dropped too, or FormControlLabel would put an asterisk on  // every option. The legend already shows one.  const {    error: _error,    helperText: _helperText,    required: _required,    ...adapter  } = useMuiCheckboxAdapter<unknown, FormValues, "channels", HTMLButtonElement>({    control,    value,    ...sharedProps,  });  return <FormControlLabel control={<Checkbox {...adapter} />} label={label} />;}// A "select all" checkbox on the same field. `checked` and `indeterminate`// read the whole array, and `transform.output` replaces it in one go.function ChannelsField({ control }: { control: Control<FormValues> }) {  const {    error,    helperText,    required: _required,    ...adapter  } = useMuiCheckboxAdapter<    unknown,    FormValues,    "channels",    HTMLButtonElement  >({    control,    value: "all",    ...sharedProps,    composeHelperText: true,    checked: (value) =>      Array.isArray(value) && value.length === CHANNELS.length,    indeterminate: (value) =>      Array.isArray(value) &&      value.length > 0 &&      value.length < CHANNELS.length,    transform: {      output: (event: React.ChangeEvent<HTMLInputElement>) =>        event.target.checked          ? CHANNELS.map((channel) => channel.value)          : undefined,    },  });  return (    <FormControl      required      error={error}      component="fieldset"      variant="standard"    >      <FormLabel component="legend">Notify me by</FormLabel>      <FormGroup>        <FormControlLabel          control={<Checkbox {...adapter} />}          label="All channels"        />        <Stack sx={{ pl: 3 }}>          {CHANNELS.map((channel) => (            <ChannelCheckbox              key={channel.value}              control={control}              value={channel.value}              label={channel.label}            />          ))}        </Stack>      </FormGroup>      {helperText ? <FormHelperText>{helperText}</FormHelperText> : null}    </FormControl>  );}export default function Page() {  const { control, handleSubmit } = useForm<FormValues>({    defaultValues: { channels: ["email"] },  });  return (    <form      noValidate      onSubmit={handleSubmit((values) =>        alert(JSON.stringify(values, null, 2)),      )}    >      <Stack direction="column" spacing={2}>        <ChannelsField control={control} />        <div>          <MuiButtonElement control={control} type="submit" variant="contained">            Save preferences          </MuiButtonElement>        </div>      </Stack>    </form>  );}

How it works

value is required. Each checkbox stands for one value. Without a value, the hook throws `value` prop is required when using type as "checkbox". This applies to single on/off checkboxes too (see below).

The field holds an array. By default, checking a box adds its value to the field's array and unchecking removes it. When the last value is removed, the field becomes undefined, not an empty array. Several checkboxes with the same name build one array together. Give them the same rules, as the demo does with sharedProps.

Checked state. The returned checked is true when the field is an array that contains value. Values are compared as strings, so a field value of "2" checks a box with value={2}. Pass a checked function to decide yourself. It receives the field value (after transform.input) and returns a boolean. indeterminate works the same way and is only applied when you pass it as a function.

Transforms. transform.output receives (event, checked, previousValue), where previousValue is the current field value. Whatever it returns is stored. Your transform replaces the default for each key you set, so overriding output alone, as the demo's "select all" box does, keeps the default input.

Single on/off checkbox. To store true/false instead of an array, give the box a value, decide checked yourself and store the checked flag:

const adapter = useMuiCheckboxAdapter<unknown, FormValues, "terms", HTMLButtonElement>({
  control,
  name: "terms",
  value: true,
  required: true,
  checked: (value) => value === true,
  transform: {
    output: (_event: React.ChangeEvent<HTMLInputElement>, checked: boolean) => checked,
  },
});

React Hook Form treats false as missing, so required here means "must be checked".

Validation. required fails when no box in the group is checked, because the field is then undefined. minLength and maxLength don't apply to arrays in React Hook Form, so use rules.validate to require "at least two" or "at most three".

Inside a FormControl. If you don't pass disabled or required, the hook takes them from the surrounding MUI FormControl.

Errors. A checkbox can't show a message. The hook returns error and helperText; render them in a FormControl and FormHelperText around the group. With composeHelperText: true, helperText is the field's error message, falling back to your helperText when there's no error.

Checkbox has no error or helperText props. Remove them before spreading the result. Remove required as well when the checkbox sits in a FormControlLabel: otherwise the label shows an asterisk next to every option.

Keep option values the same type as the stored values. Checked state compares values as strings, but unchecking removes values with strict equality. If the field holds "2" and the box has value={2}, it shows as checked but clicking it can't remove "2".

Props

Prop

Type

Return value

Prop

Type

On this page