[SOLVED] MUI Autocomplete and react-hook-form not displaying selected option with fetched data

Issue

I have a MUI Autocomplete inside a form from react hook form that works fine while filling the form, but when I want to show the form filled with fetched data, the MUI Autocomplete only displays the selected option after two renders.

I think it’s something with useEffect and reset (from react hook form), because the Autocompletes whose options are static works fine, but the ones that I also have to fetch the options from my API only works properly after the second time the useEffect runs.

I can’t reproduce a codesandbox because it’s a large project that consumes a real api, but I can provide more information if needed. Thanks in advance if someone can help me with this.

The page where I choose an item to visualize inside the form:

const People: React.FC = () => {

  const [show, setShow] = useState(false);
  const [modalData, setModalData] = useState<PeopleProps>({} as PeopleProps);

  async function showCustomer(id: string) {
    await api
      .get(`people/${id}`)
      .then((response) => {
        setModalData(response.data);
        setShow(true);
      })
      .catch((error) => toast.error('Error')
      )
  }

  return (
    <>
      {...} // there's a table here with items that onClick will fire showCustomer() 

      <Modal
        data={modalData}
        visible={show}
      />
    </>
  );
};

My form inside the Modal:

const Modal: React.FC<ModalProps> = ({data, visible}) => {
  const [situations, setSituations] = useState<Options[]>([]);
  const methods = useForm<PeopleProps>({defaultValues: data});
  const {reset} = methods;

  /* FETCH POSSIBLE SITUATIONS FROM API*/
  useEffect(() => {
    api
      .get('situations')
      .then((situation) => setSituations(situation.data.data))
      .catch((error) => toast.error('Error'));
  }, [visible]);
  /* RESET FORM TO POPULATE WITH FETCHED DATA */
  useEffect(() => reset(data), [visible]);

  return (
    <Dialog open={visible}>
      <FormProvider {...methods}>
        <DialogContent>
          <ComboBox
            name="situation_id"
            label="Situação"
            options={situations.map((item) => ({
              id: item.id,
              text: item.description
            }))}
          />
        </DialogContent>
      </FormProvider>
    </Dialog>
  );
};
export default Modal;

ComboBox component:

const ComboBox: React.FC<ComboProps> = ({name, options, ...props}) => {
  const {control, getValues} = useFormContext();

  return (
    <Controller
      name={`${name}`}
      control={control}
      render={(props) => (
        <Autocomplete
          {...props}
          options={options}
          getOptionLabel={(option) => option.text}
          getOptionSelected={(option, value) => option.id === value.id}
          defaultValue={options.find(
            (item) => item.id === getValues(`${name}`)
          )}
          renderInput={(params) => (
            <TextField
              variant="outlined"
              {...props}
              {...params}
            />
          )}
          onChange={(event, data) => {
            props.field.onChange(data?.id);
          }}
        />
      )}
    />
  );
};

export default ComboBox;

Solution

I think you simplify some things here:

  • render the <Modal /> component conditionally so you don’t have to render it when you are not using it.
  • you shouldn’t set the defaultValue for your <Autocomplete /> component as RHF will manage the state for you. So if you are resetting the form RHF will use that new value for this control.
  • it’s much easier to just use one of the fetched options as the current/default value for the <Autocomplete /> – so instead of iterating over all your options every time a change is gonna happen (and passing situation_id as the value for this control), just find the default option after you fetched the situations and use this value to reset the form. In the CodeSandbox, i renamed your control from "situation_id" to "situation". This way you only have to map "situation_id" on the first render of <Modal /> and right before you would send the edited values to your api on save.

I made a small CodeSandbox trying to reproduce your use case, have a look:

mui@v4

Edit React Hook Form - NestedValue (forked)

mui@v5

Edit React Hook Form - NestedValue MUI-v5

Another important thing: you should use useFormContext only if you have deeply nested controls, otherwise just pass the control to your <ComboBox /> component. As with using FormProvider it could affect the performance of your app if the form gets bigger and complex. From the documentation:

React Hook Form’s FormProvider is built upon React’s Context API. It solves the problem where data is passed through the component tree without having to pass props down manually at every level. This also causes the component tree to trigger a re-render when React Hook Form triggers a state update

Answered By – knoefel

Answer Checked By – Clifford M. (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *