React-hook-form: Material UI + multiple checkboxes + default selected

Created on 28 Apr 2020  ·  41Comments  ·  Source: react-hook-form/react-hook-form

I am trying to build a form which accommodates multiple 'grouped' checkboxes using Material UI.

The checkboxes are created async from a HTTP Request.

I want to provide an array of the objects IDs as the default values:

defaultValues: { boat_ids: trip?.boats.map(boat => boat.id.toString()) || [] },

Also, when I select or deselect a checkbox, I want to add/remove the ID of the object to the values of react-hook-form.

How can I achieve that?

Here is a sample that I am trying to reproduce the issue:

https://codesandbox.io/s/smoosh-dream-zmszs?file=/src/App.js

Right now with what I have tried, I get this:

Screenshot 2020-04-28 at 03 10 04

Bonus point, validation of minimum selected checkboxes using Yup

boat_ids: Yup.array() .min(2, ""),

Thanks in advance!

question

Most helpful comment

Thanks @bluebill1049, adjusted both examples and wrapped the checkboxes with a Controller.

Interested to know whether you meant it this way.

Without useState
https://codesandbox.io/s/material-demo-bzj4i?file=/demo.js

With useState
https://codesandbox.io/s/material-demo-ofk6d?file=/demo.js

All 41 comments

can you update your example with Controller instead react-hook-form-input?

Done, not sure how is the correct way though, since that would answer my question :)

This is probably a more relevant question for MUI's checkbox, but i just quicky did it with my own understanding.

https://codesandbox.io/s/serverless-dream-jm134?file=/src/App.js

@bluebill1049 I am not sure if it's MUI checkbox question.
I want to handle integers (the objects IDs), not true/false when checking/unchecking.

I have to check the doc for MUI:
https://material-ui.com/components/checkboxes/

Screen Shot 2020-04-28 at 5 46 08 pm

I am trying to do something like this:

const handleCheck = (event, isInputChecked) => {
    let checked = event[1];
    if (checked) {
      return value;
    } else {
      return null;
    }
    // onChange(event, isInputChecked, this.props.category);
  };
<FormControlLabel
      value={value}
      control={
        <Controller
          as={<Checkbox />}
          name={name}
          type="checkbox"
          value={value}
          onChange={handleCheck}
          register={register}
          control={control}
        />
      }
      label={`Boat ${value}`}
    />

But no luck,

Screenshot 2020-04-28 at 15 28 00

With chakra ui and formik that would be like this:

https://codesandbox.io/s/reverent-goldstine-1bvq9?file=/src/App.js

maybe you should try to use chakra with RHF.

Here is a working version:

import React from "react";
import { useForm, Controller } from "react-hook-form";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Checkbox from "@material-ui/core/Checkbox";

export default function CheckboxesGroup() {
  const { control, handleSubmit } = useForm({
    defaultValues: {
      bill: "bill",
      luo: ""
    }
  });

  return (
    <form onSubmit={handleSubmit(e => console.log(e))}>
      {["bill", "luo"].map(name => (
        <Controller
          key={name}
          name={name}
          as={
            <FormControlLabel
              control={<Checkbox value={name} />}
              label={name}
            />
          }
          valueName="checked"
          type="checkbox"
          onChange={([e]) => {
            return e.target.checked ? e.target.value : "";
          }}
          control={control}
        />
      ))}
      <button>Submit</button>
    </form>
  );
}

codesandbox link: https://codesandbox.io/s/material-demo-65rjy?file=/demo.js:0-932

However, I do not recommend doing so, because Checkbox in material UI probably should return checked (boolean) instead of (value).

I'd like to post another solution as well:

import React, { useState } from "react";
import { useForm, Controller } from "react-hook-form";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Checkbox from "@material-ui/core/Checkbox";

export default function CheckboxesGroup() {
  const defaultNames = ["bill", "Manos"];
  const { control, handleSubmit } = useForm({
    defaultValues: { names: defaultNames }
  });

  const [checkedValues, setCheckedValues] = useState(defaultNames);

  function handleSelect(checkedName) {
    const newNames = checkedValues?.includes(checkedName)
      ? checkedValues?.filter(name => name !== checkedName)
      : [...(checkedValues ?? []), checkedName];
    setCheckedValues(newNames);
    return newNames;
  }

  return (
    <form onSubmit={handleSubmit(e => console.log(e))}>
      {["bill", "luo", "Manos", "user120242"].map(name => (
        <FormControlLabel
          control={
            <Controller
              as={<Checkbox />}
              control={control}
              checked={checkedValues.includes(name)}
              name="names"
              onChange={() => handleSelect(name)}
            />
          }
          key={name}
          label={name}
        />
      ))}
      <button>Submit</button>
    </form>
  );
}

Codesandbox link: https://codesandbox.io/s/material-demo-639rq?file=/demo.js

i love solutions! thanks very much @4ortytwo

@bluebill1049
It seems that there is a problem with 5.6.1.

The initial states of the checkboxes break on the latest version. They are checked but their ui state is not updated.

Try to switch from 5.6.1 and 5.6.0 and refresh on that example:

https://codesandbox.io/s/autumn-pond-8tfip?file=/src/App.js

@mKontakis can you raise this as an issue?

wait didn't i supply you with a working version? (above)

You did mate, but I preferred to use that solution https://stackoverflow.com/a/61541837/2200675 to transform the values using Yup instead of modifying the returned value of Material's component CheckBox.

Although 5.6.1 breaking that solution may be a bug that should be fixed? I checked the changes between 5.6.1 and 5.6.0 I didn't manage to find something relevant

right, please create a separate issue I will do some investigation tonight.

Hey I was able to get almost the result I was looking for with @4ortytwo solution. Everything is working except for the reset(). I am using this field on a form that can edited or not and when cancelling the edit I want to reset back to the original value. Which happens just not visually.

The only way I can get this working is to stop using Material ui Checkbox and use a basic `. Just need to match styling now :disappointed:

Are there any ideas on how to get the reset functionality to work with @4ortytwo solution?

Here is a modified codesandbox which simulates what my setup is doing when fetching data with Apollo and reset in the useEffect
https://codesandbox.io/s/material-demo-0m6x5?file=/demo.js

Also note I am using this code to get the initial list value as the data comes from Apollo client

const initialList = R.path(['defaultValuesRef', 'current', name])(control)
  useEffect(() => {

    if (initialList) {
      setCheckedList(initialList)
    }
  }, [initialList])

And here is the full component for reference, note the reset is called in the parent component which handles the entire form

const CourseAvailableTo = (props) => {
  const {
    control,
    name,
    error,
    className,
    darkDisabled,
    required,
    disabled,
    checkboxes,
    label,
    headerLabel,
    register,
  } = props
  const classes = useStyles()
  const hasError = !!error
  const [checkedList, setCheckedList] = useState([])
  const initialList = R.path(['defaultValuesRef', 'current', name])(control)
  useEffect(() => {
    if (initialList) {
      setCheckedList(initialList)
    }
  }, [initialList])

  const handleChange = useCallback(
    (value) => {
      const onList = checkedList.includes(value)
      const oldList = [...checkedList]
      let newList = [...oldList]
      if (onList) {
        // remove from list
        newList = R.reject(R.equals(value))(oldList)
      } else {
        // add to list
        newList = R.append(value)(oldList)
      }
      setCheckedList(newList)
      // control.setValue(name, newList)
      return newList
    },
    [checkedList]
  )

  return (
    <div className={clsx(classes.wrapper, className)}>
      <Typography variant={headerLabel} gutterBottom>
        {`${label}:${required ? ' *' : ''}`}
      </Typography>
      <FormControl component="fieldset" className={classes.checkboxFormControl}>
        <FormGroup error={hasError} className={classes.checkboxFormGroup}>
          {checkboxes.map((checkbox, index) => (
            <FormControlLabel
              className={clsx(
                classes.checkbox,
                darkDisabled && classes.darkDisabledCheckBoxLabel
              )}
              key={checkbox.id}
              control={
                <Controller
                  as={<Checkbox />}
                  className={clsx(darkDisabled && classes.darkDisabledCheckBox)}
                  disabled={disabled}
                  value={checkbox.value}
                  onChange={(e) => handleChange(checkbox.value)}
                  checked={checkedList.includes(checkbox.value)}
                  name={`${name}`}
                  control={control}
                />
                // <input
                //   type="checkbox"
                //   className={clsx(darkDisabled && classes.darkDisabledCheckBox)}
                //   disabled={disabled}
                //   name={name}
                //   value={checkbox.value}
                //   ref={register}
                // />
              }
              label={checkbox.label}
            />
          ))}
        </FormGroup>
        <FormHelperText error={hasError} className={classes.errorMessage}>
          {hasError && error}
        </FormHelperText>
      </FormControl>
      <Button
        type="button"
        className={classes.selectAllButton}
        variant="outlined"
        color="primary"
        disabled={disabled}
        onClick={() => {
          if (R.length(checkedList) <= R.length(checkboxes) - 1) {
            control.setValue(name, R.pluck('value')(checkboxes))
            return setCheckedList(R.pluck('value')(checkboxes))
          }
          control.setValue(name, [])
          return setCheckedList([])
        }}
      >
        {`${
          R.length(checkedList) <= R.length(checkboxes) - 1
            ? 'Select All'
            : 'Deselect All'
        }`}
      </Button>
    </div>
  )
}

@natac13 , your checked state depends on the checkedValues array and it wasn't getting updated when you reset the form.

I added this:

 const handleReset = () => {
    setCheckedValues(defaultNames);
    reset();
  };

and called it here

<button onClick={handleReset}>Reset</button>

See the codesandbox, it is working there: https://codesandbox.io/s/material-demo-0bwsi?file=/demo.js

That works! Thanks @4ortytwo. However I would have to move the setCheckedValues function out of my custom field component to the form component, then pass in the state and state update function. This then causes the form component to re-render!! :scream: If I lose Material ui and use the example found here almost no re-render happens! Which is the goal of rewriting all my forms from Formik to react-hook-forms.

Again thank you for the solution which works great! However I think I may just make a custom checkbox component which using <input type="checkbox"/> and use the checkbox hack

Also this library is amazing! No re-renders on inputs! Just hard to use Material ui. At least that is what I am finding moving from Formik.

That works! Thanks @4ortytwo. However I would have to move the setCheckedValues function out of my custom field component to the form component, then pass in the state and state update function. This then causes the form component to re-render!! 😱 If I lose Material ui and use the example found here almost no re-render happens! Which is the goal of rewriting all my forms from Formik to react-hook-forms.

Again thank you for the solution which works great! However I think I may just make a custom checkbox component which using <input type="checkbox"/> and use the checkbox hack

Also this library is amazing! No re-renders on inputs! Just hard to use Material ui. At least that is what I am finding moving from Formik.

V6 will make this exp easier, with render props (Controller).

For anyone looking at this with v6.x.x, the workarounds here do not work at all in v6.01 currently

For anyone looking at this with v6.x.x, the workarounds here do not work at all in v6.01 currently

use render prop it should be much easier now.

Thanks for sharing @4ortytwo

Again, issues with this use-case on v6.0.8.

Let's recap:

Let's say that I have an array of objects as such:

const items = [
  {
    id: 0,
    name: "Object 0"
  },
  {
    id: 1,
    name: "Object 1"
  },
  {
    id: 2,
    name: "Object 2"
  },
  {
    id: 3,
    name: "Object 3"
  },
  {
    id: 4,
    name: "Object 4"
  }
];

Each object corresponds to one checkbox. The user should be able to select multiple and get a result in the form of:
item_ids: [1, 3, 4], with the IDs in the array being the selected checkboxes.
The group of the checkboxes should have initial states.

I am trying to use the render method, but it looks like something is wrong with the value. When i select checkboxes i get strings instead of booleans.

I tried some workarounds but nothing works, the array is not populated/removed properly.

Here is the codesandbox:

https://codesandbox.io/s/material-demo-2wvuq?file=/demo.js

@bluebill1049 Maybe you have an opinion on that?

@mKontakis, I'm not Bill but came up with a couple of solutions.

With and without useState.

Adjusted your Yup resolver as well, it filters out falsy fields now but would transform your values into an array of indices otherwise.

WITHOUT useState:

https://codesandbox.io/s/material-demo-bzj4i?file=/demo.js

WITH useState:

https://codesandbox.io/s/material-demo-ofk6d?file=/demo.js

With useState is probably redundant now that it can be done without it, just wanted to show that it's possible, too.

Thanks @4ortytwo, @mKontakis i will take a look at this one. maybe consider staying at the old version (if works for you) until i get you a solution.

wow @4ortytwo your example works great, i think it would probably easier to build a controlled component which wrapt those checkbox and a single Controller just to collect value instead map around Conrtroller

Thanks @bluebill1049, adjusted both examples and wrapped the checkboxes with a Controller.

Interested to know whether you meant it this way.

Without useState
https://codesandbox.io/s/material-demo-bzj4i?file=/demo.js

With useState
https://codesandbox.io/s/material-demo-ofk6d?file=/demo.js

nice @4ortytwo yeah! personally I think it's cleaner with a single Controller. Thanks for making those CSBs. ❤️

It is cleaner, indeed. My pleasure, @bluebill1049! Thanks for the great library! :)

Thanks @bluebill1049, adjusted both examples and wrapped the checkboxes with a Controller.

Interested to know whether you meant it this way.

Without useState
https://codesandbox.io/s/material-demo-bzj4i?file=/demo.js

With useState
https://codesandbox.io/s/material-demo-ofk6d?file=/demo.js

@4ortytwo @bluebill1049 Hello, Thanks for the solutions with MuiCheckboxes without useState. How can we do validation without yup library by just providing rules from RHF ?

Thanks @bluebill1049, adjusted both examples and wrapped the checkboxes with a Controller.
Interested to know whether you meant it this way.
Without useState
https://codesandbox.io/s/material-demo-bzj4i?file=/demo.js
With useState
https://codesandbox.io/s/material-demo-ofk6d?file=/demo.js

@4ortytwo @bluebill1049 Hello, Thanks for the solutions with MuiCheckboxes without useState. How can we do validation without yup library by just providing rules from RHF ?

you can use the validate function right?

Thanks @bluebill1049, adjusted both examples and wrapped the checkboxes with a Controller.
Interested to know whether you meant it this way.
Without useState
https://codesandbox.io/s/material-demo-bzj4i?file=/demo.js
With useState
https://codesandbox.io/s/material-demo-ofk6d?file=/demo.js

@4ortytwo @bluebill1049 Hello, Thanks for the solutions with MuiCheckboxes without useState. How can we do validation without yup library by just providing rules from RHF ?

you can use the validate function right?

@bluebill1049 Yes. We can add validate function but how can we attach custom message to that ?

what do you mean custom message @sandeepkumar-vedam-by? can't you produce different messages based on the selection from validate function?

@bluebill1049 Here is the validate function and when value length is 0 i want to display error message like 'Select atleast one item'

rules: {
   validate: (value: any) => {
       return value.length > 0 ? true: false;
    },
}

Generally, we can add message to rules like below
rules: { required: { value: true, message: 'Email is Required' }, pattern: { value: /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/, message: "Invalid email address" }, minLength: { value: 5, message: 'First Name must be atleast 2 characters' } }

you can have a switch statement inside right? based on your array value return specific error message.

you can have a switch statement inside right? based on your array value return specific error message.

@bluebill1049 Yes. It is working. I didn't think that we can directly return message in validate function. Thanks :)

If not asking too much, could you do your examples with Typescript @4ortytwo ? It's almost there, the render function complains here about this:

Type '(props: { onChange: (...event: any[]) => void; onBlur: () => void; value: any; name: string; }) => Element[]' is not assignable to type '(data: { onChange: (...event: any[]) => void; onBlur: () => void; value: any; name: string; }) => ReactElement<any, string | ((props: any) => ReactElement<any, any> | null) | (new (props: any) => Component<any, any, any>)>'.
  Type 'Element[]' is missing the following properties from type 'ReactElement<any, string | ((props: any) => ReactElement<any, any> | null) | (new (props: any) => Component<any, any, any>)>': type, props, keyts(2322)
controller.d.ts(6, 5): The expected type comes from property 'render' which is declared here on type '(IntrinsicAttributes & { as: "input" | "select" | "textarea" | FunctionComponent<any> | ComponentClass<any, any> | ReactElement<...>; render?: undefined; } & { ...; } & Pick<...>) | (IntrinsicAttributes & ... 2 more ... & Pick<...>)'

I'm unsure how to fix it. It looks like the return from render is expected to be something else than an array so when you put the array there, the type won't validate.

EDIT: I do think it's a problem with the render prop, could anyone confirm?

If not asking too much, could you do your examples with Typescript @4ortytwo ? It's almost there, the render function complains here about this:

Type '(props: { onChange: (...event: any[]) => void; onBlur: () => void; value: any; name: string; }) => Element[]' is not assignable to type '(data: { onChange: (...event: any[]) => void; onBlur: () => void; value: any; name: string; }) => ReactElement<any, string | ((props: any) => ReactElement<any, any> | null) | (new (props: any) => Component<any, any, any>)>'.
  Type 'Element[]' is missing the following properties from type 'ReactElement<any, string | ((props: any) => ReactElement<any, any> | null) | (new (props: any) => Component<any, any, any>)>': type, props, keyts(2322)
controller.d.ts(6, 5): The expected type comes from property 'render' which is declared here on type '(IntrinsicAttributes & { as: "input" | "select" | "textarea" | FunctionComponent<any> | ComponentClass<any, any> | ReactElement<...>; render?: undefined; } & { ...; } & Pick<...>) | (IntrinsicAttributes & ... 2 more ... & Pick<...>)'

I'm unsure how to fix it. It looks like the return from render is expected to be something else than an array so when you put the array there, the type won't validate.

EDIT: I do think it's a problem with the render prop, could anyone confirm?

I also got this same error, this is because the render prop does not allow an array of JSX elements. So I just wrapped my mapped JSX with a fragment. This managed to "fool" typescript

I just threw a @ts-ignore in that line because I intentionally want to go back to this issue later (and it is a hack).
If you could please update the render method to also accept JSX.Element[] of whatever is accepting now.

Nice fix too @ahmedrafayat

This is my final element:

<Controller
  name="ids"
  control={control}
  // @ts-ignore
  // <Controller /> render doesnt expect an array of elements (JSX.Element[])
  // BUT it works regardless. So I'm ignoring TS typecheck here until it's fixed
  render={() =>
    arrayOfElements.map((element) => (
      <FormControlLabel
        control={
          <Checkbox
            onChange={() => handleCheck(element.id)}
            checked={checkedValues.includes(element.id)}
          />
        }
        key={element.id}
        label={element.action}
      />
    ))
  }
/>
Was this page helpful?
0 / 5 - 0 ratings