60

I am trying to use a custom Material UI Autocomplete component and connect it to react-hook-form.

TLDR: Need to use Material UI Autocomplete with react-hook-form Controller without defaultValue

My custom Autocomplete component takes an object with the structure {_id:'', name: ''} it displays the name and returns the _id when an option is selected. The Autocomplete works just fine.

<Autocomplete
  options={options}
  getOptionLabel={option => option.name}
  getOptionSelected={(option, value) => option._id === value._id}
  onChange={(event, newValue, reason) => {
    handler(name, reason === 'clear' ? null : newValue._id);
  }}
  renderInput={params => <TextField {...params} {...inputProps} />}
/>

In order to make it work with react-hook-form I've set the setValues to be the handler for onChange in the Autocomplete and manually register the component in an useEffect as follows

useEffect(() => {
  register({ name: "country1" });
},[]);

This works fine but I would like to not have the useEffect hook and just make use of the register somehow directly.

Next, I tried to use the Controller component from react-hook-form to properly register the field in the form and not to use the useEffect hook

<Controller
  name="country2"
  as={
    <Autocomplete
      options={options}
      getOptionLabel={option => option.name}
      getOptionSelected={(option, value) => option._id === value._id}
      onChange={(event, newValue, reason) =>
        reason === "clear" ? null : newValue._id
      }
      renderInput={params => (
        <TextField {...params} label="Country" />
      )}
    />
  }
  control={control}
/>

I've changed the onChange in the Autocomplete component to return the value directly but it doesn't seem to work.

Using inputRef={register} on the <TextField/> would not cut it for me because I want to save the _id and not the name

HERE is a working sandbox with the two cases. The first with useEffect and setValue in the Autocomplete my works. The second my attempt in using Controller component

Any help is appreciated.

LE

After the comment from Bill with the working sandbox of MUI Autocomplete, I Managed to get a functional result

<Controller
  name="country"
  as={
    <Autocomplete
      options={options}
      getOptionLabel={option => option.name}
      getOptionSelected={(option, value) => option._id === value._id}
      renderInput={params => <TextField {...params} label="Country" />}
    />
  }
  onChange={([, { _id }]) => _id}
  control={control}
/>

The only problem is that I get an MUI Error in the console

Material-UI: A component is changing the uncontrolled value state of Autocomplete to be controlled.

I've tried to set an defaultValue for it but it still behaves like that. Also, I would not want to set a default value from the options array due to the fact that these fields in the form are not required.

The updated sandbox HERE

Any help is still very much appreciated

4
  • 1
    codesandbox.io/s/react-hook-form-controller-079xx have you seen this? Commented May 8, 2020 at 0:45
  • @Bill thank you for the link, I went through it's a working example but I still face some other issues now related to the state of the autocomplete component. I've updated the question with a LE. Thank you Commented May 10, 2020 at 8:39
  • if you follow what's in the codesandbox, it should resolve the problem, right? Commented May 10, 2020 at 10:06
  • codesandbox.io/s/… Commented Aug 19, 2021 at 6:43

8 Answers 8

34

The accepted answer (probably) works for the bugged version of Autocomplete. I think the bug was fixed some time after that, so that the solution can be slightly simplified.

This is very useful reference/codesandbox when working with react-hook-form and material-ui: https://codesandbox.io/s/react-hook-form-controller-601-j2df5?

From the above link, I modified the Autocomplete example:

import TextField from '@material-ui/core/TextField';
import Autocomplete from '@material-ui/lab/Autocomplete';


const ControlledAutocomplete = ({ options = [], renderInput, getOptionLabel, onChange: ignored, control, defaultValue, name, renderOption }) => {
  return (
    <Controller
      render={({ onChange, ...props }) => (
        <Autocomplete
          options={options}
          getOptionLabel={getOptionLabel}
          renderOption={renderOption}
          renderInput={renderInput}
          onChange={(e, data) => onChange(data)}
          {...props}
        />
      )}
      onChange={([, data]) => data}
      defaultValue={defaultValue}
      name={name}
      control={control}
    />
  );
}

With the usage:

<ControlledAutocomplete
    control={control}
    name="inputName"
    options={[{ name: 'test' }]}
    getOptionLabel={(option) => `Option: ${option.name}`}
    renderInput={(params) => <TextField {...params} label="My label" margin="normal" />}
    defaultValue={null}
/>

control is from the return value of useForm(}

Note that I'm passing null as defaultValue as in my case this input is not required. If you'll leave defaultValue you might get some errors from material-ui library.

UPDATE:

Per Steve question in the comments, this is how I'm rendering the input, so that it checks for errors:

renderInput={(params) => (
                  <TextField
                    {...params}
                    label="Field Label"
                    margin="normal"
                    error={errors[fieldName]}
                  />
                )}

Where errors is an object from react-hook-form's formMethods:

const { control, watch, errors, handleSubmit } = formMethods
Sign up to request clarification or add additional context in comments.

3 Comments

I'm curious how your handling required autocomplete fields - for me with render it doesn't seem to trigger the errors
@Steve - I can see multiple reasons why the errors aren't showing up, depending on: validation library and validation schema, component structure, field naming (e.g. you need to use indexes in the field name if you're using array field). If you'll provide more details about your case, I might tailor the response. In my case, I'm using yup for validation, so the field has .required() in its schema. I'll also add a quick example how I'm rendering the input, so that error is shown. Let me know, if it's helpful.
Is there a way to do that without the renderInput prop? like just adding the register to the Autocomplete like so: {...register("myFieldName")} ?
14
import { Button } from "@material-ui/core";
import Autocomplete from "@material-ui/core/Autocomplete";
import { red } from "@material-ui/core/colors";
import Container from "@material-ui/core/Container";
import CssBaseline from "@material-ui/core/CssBaseline";
import { makeStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import AdapterDateFns from "@material-ui/lab/AdapterDateFns";
import LocalizationProvider from "@material-ui/lab/LocalizationProvider";
import React, { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";


export default function App() {
  const [itemList, setItemList] = useState([]);
  // const classes = useStyles();

  const {
    control,
    handleSubmit,
    setValue,
    formState: { errors }
  } = useForm({
    mode: "onChange",
    defaultValues: { item: null }
  });

  const onSubmit = (formInputs) => {
    console.log("formInputs", formInputs);
  };

  useEffect(() => {
    setItemList([
      { id: 1, name: "item1" },
      { id: 2, name: "item2" }
    ]);
    setValue("item", { id: 3, name: "item3" });
  }, [setValue]);

  return (
    <LocalizationProvider dateAdapter={AdapterDateFns}>
      <Container component="main" maxWidth="xs">
        <CssBaseline />

        <form onSubmit={handleSubmit(onSubmit)} noValidate>
          <Controller
            control={control}
            name="item"
            rules={{ required: true }}
            render={({ field: { onChange, value } }) => (
              <Autocomplete
                onChange={(event, item) => {
                  onChange(item);
                }}
                value={value}
                options={itemList}
                getOptionLabel={(item) => (item.name ? item.name : "")}
                getOptionSelected={(option, value) =>
                  value === undefined || value === "" || option.id === value.id
                }
                renderInput={(params) => (
                  <TextField
                    {...params}
                    label="items"
                    margin="normal"
                    variant="outlined"
                    error={!!errors.item}
                    helperText={errors.item && "item required"}
                    required
                  />
                )}
              />
            )}
          />

          <button
            onClick={() => {
              setValue("item", { id: 1, name: "item1" });
            }}
          >
            setValue
          </button>

          <Button
            type="submit"
            fullWidth
            size="large"
            variant="contained"
            color="primary"
            // className={classes.submit}
          >
            submit
          </Button>
        </form>
      </Container>
    </LocalizationProvider>
  );
}

1 Comment

13

So, I fixed this. But it revealed what I believe to be an error in Autocomplete.

First... specifically to your issue, you can eliminate the MUI Error by adding a defaultValue to the <Controller>. But that was only the beginning of another round or problems.

The problem is that functions for getOptionLabel, getOptionSelected, and onChange are sometimes passed the value (i.e. the _id in this case) and sometimes passed the option structure - as you would expect.

Here's the code I finally came up with:

import React from "react";
import { useForm, Controller } from "react-hook-form";
import { TextField } from "@material-ui/core";
import { Autocomplete } from "@material-ui/lab";
import { Button } from "@material-ui/core";
export default function FormTwo({ options }) {
  const { register, handleSubmit, control } = useForm();

  const getOpObj = option => {
    if (!option._id) option = options.find(op => op._id === option);
    return option;
  };

  return (
    <form onSubmit={handleSubmit(data => console.log(data))}>
      <Controller
        name="country"
        as={
          <Autocomplete
            options={options}
            getOptionLabel={option => getOpObj(option).name}
            getOptionSelected={(option, value) => {
              return option._id === getOpObj(value)._id;
            }}
            renderInput={params => <TextField {...params} label="Country" />}
          />
        }
        onChange={([, obj]) => getOpObj(obj)._id}
        control={control}
        defaultValue={options[0]}
      />
      <Button type="submit">Submit</Button>
    </form>
  );
}

4 Comments

Thank you for taking the time to answer. Setting a default value is not an option for me because the fields are not mandatory therefore they must be empty. Also I forked my sandbox and updated with your code. The clear option breaks the app, check it codesandbox.io/s/busy-forest-tmb6s
Both problems fixed easily enough. Just need to allow for an empty/null option in getOpObj and getOptionSelected. You can then pass null as the defaultOption.
CodeSandbox won't let me sign-in for some reason. Didn't know you could save forked sandboxes without signing in. But this appears to have worked. Check out: codesandbox.io/s/angry-bogdan-w9eg8
11

I do not know why the above answers did not work for me, here is the simplest code that worked for me, I used render function of Controller with onChange to change the value according to the selected one.

<Controller
  control={control}
  name="type"
  rules={{
    required: 'Veuillez choisir une réponse',
  }}
  render={({ field: { onChange, value } }) => (
    <Autocomplete
      freeSolo
      options={['field', 'select', 'multiple', 'date']}
      onChange={(event, values) => onChange(values)}
      value={value}
      renderInput={(params) => (
        <TextField
          {...params}
          label="type"
          variant="outlined"
          onChange={onChange}
        />
      )}
    />
  )}
/>

Comments

5

I have made it work pretty well including multiple tags selector as follow bellow. It will work fine with mui5 and react-hook-form 7

import { useForm, Controller } from 'react-hook-form';
import Autocomplete from '@mui/material/Autocomplete';

//setup your form and control

<Controller
    control={control}
    name="yourFiledSubmitName"
    rules={{
        required: 'required field',
    }}
    render={({ field: { onChange } }) => (
        <Autocomplete
            multiple
            options={yourDataArray}
            getOptionLabel={(option) => option.label}
            onChange={(event, item) => {
                onChange(item);
            }}
            renderInput={(params) => (
                <TextField {...params} label="Your label" placeholder="Your placeholder"
                />
            )}
    )}
/>

Comments

3

Here's a TypeScript version:

import { Autocomplete, AutocompleteProps, TextField } from '@mui/material';
import React, { ReactNode } from 'react';
import { useController } from 'react-hook-form';

export interface FormAutoCompleteProps<
    T,
    Multiple extends boolean | undefined,
    DisableClearable extends boolean | undefined,
    FreeSolo extends boolean | undefined = undefined,
> extends Omit<AutocompleteProps<T | string, Multiple, DisableClearable, FreeSolo>, 'renderInput'> {
    renderInput?: AutocompleteProps<T | string, Multiple, DisableClearable, FreeSolo>['renderInput'];
    label?: ReactNode;
    name: string;
}

export default function FormAutoComplete<
    T,
    Multiple extends boolean | undefined = undefined,
    DisableClearable extends boolean | undefined = undefined,
    FreeSolo extends boolean | undefined = undefined,
>({ name, label, ...props }: FormAutoCompleteProps<T, Multiple, DisableClearable, FreeSolo>) {
    const {
        field,
        fieldState: { error },
    } = useController({ name });

    return (
        <Autocomplete
            {...field}
            onChange={(_, value) => field.onChange(value)}
            renderInput={(params) => <TextField {...params} helperText={error?.message} error={!!error} label={label} />}
            {...props}
        />
    );
}

Usage:

<FormAutoComplete
    name="days"
    label="Label here"
    multiple
    options={["a", "b", "c"]}
/>

1 Comment

To use this, you must wrap your form in FormProvider. useController calls useFormContext, which needs FormProvider as described here: react-hook-form.com/docs/useformcontext
2

Thanks to all the other answers, as of April 15 2022, I was able to figure out how to get this working and render the label in the TextField component:

const ControlledAutocomplete = ({
  options,
  name,
  control,
  defaultValue,
  error,
  rules,
  helperText,
}) => (
  <Controller
    name={name}
    control={control}
    defaultValue={defaultValue}
    rules={rules}
    render={({ field }) => (
      <Autocomplete
        disablePortal
        options={options}
        getOptionLabel={(option) =>
          option?.label ??
          options.find(({ code }) => code === option)?.label ??
          ''
        }
        {...field}
        renderInput={(params) => (
          <TextField
            {...params}
            error={Boolean(error)}
            helperText={helperText}
          />
        )}
        onChange={(_event, data) => field.onChange(data?.code ?? '')}
      />
    )}
  />
);

ControlledAutocomplete.propTypes = {
  options: PropTypes.arrayOf({
    label: PropTypes.string,
    code: PropTypes.string,
  }),
  name: PropTypes.string,
  control: PropTypes.func,
  defaultValue: PropTypes.string,
  error: PropTypes.object,
  rules: PropTypes.object,
  helperText: PropTypes.string,
};

In my case, options is an array of {code: 'US', label: 'United States'} objects. The biggest difference is the getOptionLabel, which I guess needs to account for if both when you have the list open (and option is an object) and when the option is rendered in the TextField (when option is a string) as well as when nothing is selected.

1 Comment

First of all you check for the existence of the label how many times in this answer? getOptionLabel={(option) => options.find(({ code }) => code === option)?.label ?? ''} wouldn't be enough? Also in the option you get the option directly why look into the original array for it's label? What am I missing?
-3

Instead of using controller, with the help of register, setValue of useForm and value, onChange of Autocomplete we can achieve the same result.

const [selectedCaste, setSelectedCaste] = useState([]);
const {register, errors, setValue} = useForm();

useEffect(() => {
  register("caste");
}, [register]);

return (
                <Autocomplete
                  multiple
                  options={casteList}
                  disableCloseOnSelect
                  value={selectedCaste}
                  onChange={(_, values) => {
                    setSelectedCaste([...values]);
                    setValue("caste", [...values]);
                  }}
                  getOptionLabel={(option) => option}
                  renderOption={(option, { selected }) => (
                    <React.Fragment>
                      <Checkbox
                        icon={icon}
                        checkedIcon={checkedIcon}
                        style={{ marginRight: 8 }}
                        checked={selected}
                      />
                      {option}
                    </React.Fragment>
                  )}
                  style={{ width: "100%" }}
                  renderInput={(params) => (
                    <TextField
                      {...params}
                      id="caste"
                      error={!!errors.caste}
                      helperText={errors.caste?.message}
                      variant="outlined"
                      label="Select caste"
                      placeholder="Caste"
                    />
                  )}
                />
);

2 Comments

We can, but we change the complexity of the component...
This appears more straight forward, but the method mentioned above would allow it to be much more scalable and easier to reuse across the app. Since the Control component is defined in the dedicated ControlledAutoComplete file, we can import that into any form across the app and simply pass in the control and form specific options from that form's file. This is especially handy if an api call in the forms file is retrieving the options for the AutoComplete.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.