0

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;

1 Answer 1

2

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

Sign up to request clarification or add additional context in comments.

12 Comments

It worked for getting the fetched value as default, thanks for that! But now the <Autocomplete /> expects an object as value, and RHF sets the value as a number in onChange. If you try to select another option after opening the <Modal /> in your CodeSandbox you'll see it.
If I use props.onChange(data) it works, but do you know if theres a way of keep using only data.id as the field value, instead of the whole object?
Yes, that's right. I forgot to update the onChange handler from your code when i made the CodeSandbox. I updated the CodeSandbox and the onChange handler of the <ComboBox /> component.
If you really need to use only the ID as a value for the <Autocomplete /> have a look at this CodeSandbox. I think this is what you want.
Exactly what I wanted, thanks bud! I hope I can help you someday too.
|

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.