0
votes

I have a multi-step (2) form that uses Material UI and react-hook-form. On the first step, I ask for a string input (Q1) and an address (Q2). Then, the user clicks next to move onto the second step.

At this point, I use react context to save the data in a global state and pass its values to step 2 on the form but, only the value of Q1 is saved correctly. Q2's value is saved as undefined.

Simplified code

Step 1 page

//imports here
const StepOne= () => {
  const { setValues } = useData();

  const methods = useForm({
    mode: "all",
    shouldUnregister: true
  });

  const { handleSubmit, control } = methods;

  const history = useHistory();

  const onSubmit = async (data) => {
    setValues({ address: data.address, other: data.other });
    history.push("/step2");
  };

  return (
    <MainConatiner>
      <Typography variant="h4" component="h2" gutterBottom>
        Step one
      </Typography>
      <FormProvider {...methods}>
        <form noValidate onSubmit={handleSubmit(onSubmit)} autoComplete="off">
          <AddressInput label="Address" name="address" control={control} />
          <Input name="other" label="Other input" control={control} />
          <Box mt={2}>
            <Button type="submit" size="large" variant="contained">
              next
            </Button>
          </Box>
        </form>
      </FormProvider>
    </MainConatiner>
  );
};

Autocomplete compoenent

const AddressInput = ({ control, name, ...rest }) => {
  const [value, setValue] = React.useState(null);
  const [inputValue, setInputValue] = React.useState("");
  const [options, setOptions] = React.useState([]);
  const fetch = React.useMemo(
    () =>
      throttle((request) => {
        const getData = getAddress(request?.input);
        getData.then((res) => {
          setOptions(JSON.parse(res)?.candidates);
          return JSON.parse(res).candidates;
        });
      }, 200),
    []
  );

  React.useEffect(() => {
    let active = true;
    if (inputValue === "") {
      setOptions(value ? [value] : []);
      return undefined;
    }

    fetch({ input: inputValue }, (results) => {
      if (active) {
        let newOptions = [];
        if (value) {
          newOptions = [value];
        }
        if (results) {
          newOptions = [...newOptions, ...results];
        }
        setOptions(newOptions);
      }
    });

    return () => {
      active = false;
    };
  }, [value, inputValue, fetch]);

  return (
    <Controller
      name={name}
      control={control}
      render={({ field }) => (
        <Autocomplete
          {...field}
          id="address"
          getOptionLabel={(option) =>
            typeof option === "string" ? option : option.address
          }
          filterOptions={(x) => x}
          options={options}
          autoComplete
          includeInputInList
          filterSelectedOptions
          value={value}
          onChange={(event, newValue) => {
            console.log(newValue);
            setOptions(newValue ? [newValue, ...options] : options);
            setValue(newValue);
          }}
          onInputChange={(event, newInputValue) => {
            setInputValue(newInputValue);
          }}
          renderInput={(params) => (
            <TextField {...rest} {...params} variant="outlined" fullWidth />
          )}
          renderOption={(option) => <span>{option.address}</span>}
        />
      )}
    />
  );
};

You can find the full code here: https://codesandbox.io/s/multi-step-form-4cgj9?file=/src/pages/Home.js:363-1313

Any ideas?

1

1 Answers

1
votes

There are two problems:

  1. You are spreading in the {...field} onto the Autocomplete, this includes the onChange field which you overwrite later on. Therefore you have to call the field.onChange(newValue) within you own onChange. Currently react-hook-forms never gets set to the new value.
  2. You should add the getOptionSelected to the Autocomplete so it can compare the options and check which one is selected.

The AddressInput then looks like this:

import React from "react";
import TextField from "@material-ui/core/TextField";
import Autocomplete from "@material-ui/lab/Autocomplete";
import throttle from "lodash/throttle";
import getAddress from "./api/getAddress";
import { Controller } from "react-hook-form";

const AddressInput = ({ control, name, ...rest }) => {
  const [value, setValue] = React.useState(null);
  const [inputValue, setInputValue] = React.useState("");
  const [options, setOptions] = React.useState([]);
  const fetch = React.useMemo(
    () =>
      throttle((request) => {
        const getData = getAddress(request?.input);
        getData.then((res) => {
          setOptions(JSON.parse(res)?.candidates);
          return JSON.parse(res).candidates;
        });
      }, 200),
    []
  );

  React.useEffect(() => {
    let active = true;
    if (inputValue === "") {
      setOptions(value ? [value] : []);
      return undefined;
    }

    fetch({ input: inputValue }, (results) => {
      if (active) {
        let newOptions = [];
        if (value) {
          newOptions = [value];
        }
        if (results) {
          newOptions = [...newOptions, ...results];
        }
        setOptions(newOptions);
      }
    });

    return () => {
      active = false;
    };
  }, [value, inputValue, fetch]);

  return (
    <Controller
      name={name}
      control={control}
      render={({ field }) => (
        <Autocomplete
          {...field}
          id="address"
          getOptionLabel={(option) =>
            typeof option === "string" ? option : option.address
          }
          // Add this prop to help with identifying the selected option
          getOptionSelected={(optionA, optionB) =>
            optionA.address === optionB.address
          }
          filterOptions={(x) => x}
          options={options}
          autoComplete
          includeInputInList
          filterSelectedOptions
          value={value}
          onChange={(event, newValue) => {
            console.log('newValue', newValue);
            setOptions(newValue ? [newValue, ...options] : options);
            setValue(newValue);

            // Actually change the state of react-hook-forms
            field.onChange(newValue);
          }}
          onInputChange={(event, newInputValue) => {
            setInputValue(newInputValue);
          }}
          renderInput={(params) => (
            <TextField {...rest} {...params} variant="outlined" fullWidth />
          )}
          renderOption={(option) => <span>{option.address}</span>}
        />
      )}
    />
  );
};

export default AddressInput;

Updated codesandbox