Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Dynamic form returns the same value for the first and second input

I’m creating a custom dynamic form using Material UI https://mui.com/ library as the component for my React Js app.

Here is the initial state of the dynamic form component.
enter image description here

As we can see that the dynamic form component starts only with one delivery point form.
We could add another delivery point form by clicking the "Add Delivery" button.
Here is the example after clicking the "Add Delivery" button.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

enter image description here

The problem is: the first form and the second form have the same values (if there are more than 1 form) after we gave input to one of the first or second forms as seen in the screenshot below. This is not the condition I want.

enter image description here

But the rest (third, fourth, and so on) forms are unique as seen in the screenshot below. This is the condition I want.

enter image description here

Here is the simple working code:

DeliveryPoint.jsx

const DeliveryPoint = (props) => {
  const {
    initialFormObject,
    formObject,
    setFormObject,
    collapseObject,
    setCollapseObject
  } = props;

  const classes = useStyles();

  const deliveryPointBaseObject = initialFormObject.deliveryPointList[0];

  const handleFormObjectChange = (inputEvent, inputIndex) => {
    let deliveryPointListData = [...formObject.deliveryPointList];
    deliveryPointListData[inputIndex][inputEvent.target.name] =
      inputEvent.target.value;

    setFormObject((current) => ({
      ...current,
      deliveryPointList: deliveryPointListData
    }));
  };

  const handleDatePickerChange = (inputNewValue, inputIndex) => {
    let deliveryPointListData = [...formObject.deliveryPointList];
    deliveryPointListData[inputIndex].deliveryDate = inputNewValue;

    setFormObject((current) => ({
      ...current,
      deliveryPointList: deliveryPointListData
    }));
  };

  const handleCollapseChange = (inputIndex) => {
    let deliveryPointListData = [...collapseObject.deliveryPointList];
    deliveryPointListData[inputIndex] = !deliveryPointListData[inputIndex];

    setCollapseObject((current) => ({
      ...current,
      deliveryPointList: deliveryPointListData
    }));
  };

  const handleAddFormItemButtonClick = () => {
    let deliveryPointListData = [...formObject.deliveryPointList];
    deliveryPointListData.push(deliveryPointBaseObject);

    setFormObject((current) => ({
      ...current,
      deliveryPointList: deliveryPointListData
    }));

    deliveryPointListData = [...collapseObject.deliveryPointList];
    deliveryPointListData.push(false);

    setCollapseObject((current) => ({
      ...current,
      deliveryPointList: deliveryPointListData
    }));
  };

  const handleDeleteFormItemButtonClick = (inputIndex) => {
    let deliveryPointListData = [...formObject.deliveryPointList];
    deliveryPointListData.splice(inputIndex, 1);

    setFormObject((current) => ({
      ...current,
      deliveryPointList: deliveryPointListData
    }));

    deliveryPointListData = [...collapseObject.deliveryPointList];
    deliveryPointListData.splice(inputIndex, 1);

    setCollapseObject((current) => ({
      ...current,
      deliveryPointList: deliveryPointListData
    }));
  };

  return (
    <>
      {formObject.deliveryPointList.map((item, index) => (
        <Box key={index} className={classes.formItemContainer}>
          <Box className={classes.formItemTitleContainer}>
            {/* TITLE */}
            <Typography variant="h6">
              {index === 0 ? `Delivery point` : `#${index + 1} Delivery point`}
            </Typography>

            {/* ADD OR DELETE BUTTON */}
            {index === 0 ? (
              <Button
                className={classes.formItemTitleButton}
                variant="outlined"
                startIcon={<IconAdd />}
                onClick={handleAddFormItemButtonClick}
              >
                Add Delivery
              </Button>
            ) : (
              <Button
                className={classes.formItemTitleButton}
                variant="outlined"
                startIcon={<IconRemove />}
                color="error"
                onClick={() => handleDeleteFormItemButtonClick(index)}
              >
                Remove Delivery
              </Button>
            )}
          </Box>

          {/* CONSIGNEE */}
          <FormControl
            required
            variant="outlined"
            className={classes.formItemInput}
          >
            <InputLabel>Consignee</InputLabel>

            <OutlinedInput
              label="Consignee"
              type="text"
              name="consignee"
              value={item.consignee}
              onChange={(event) => handleFormObjectChange(event, index)}
            />

            <FormHelperText>
              Search for name, street, city, or state by typing in the box.
            </FormHelperText>
          </FormControl>

          {/* DELIVERY DATE */}
          <LocalizationProvider dateAdapter={AdapterDateFns}>
            <DatePicker
              disableFuture
              label="Select Delivery Date"
              openTo="year"
              views={["year", "month", "day"]}
              value={item.deliveryDate}
              onChange={(newValue) => handleDatePickerChange(newValue, index)}
              renderInput={(params) => (
                <TextField
                  required
                  className={classes.formItemInput}
                  {...params}
                />
              )}
            />
          </LocalizationProvider>

          {/* COLLAPSE */}
          <Collapse
            in={collapseObject.deliveryPointList[index]}
            timeout="auto"
            unmountOnExit
            className={classes.formItemCollapse}
          >
            {/* DELIVERY INSTRUCTION */}
            <FormControl variant="outlined" className={classes.formItemInput}>
              <InputLabel>Delivery Instructions</InputLabel>

              <OutlinedInput
                label="Delivery Instructions"
                type="text"
                name="deliveryInstruction"
                value={item.deliveryInstruction}
                onChange={(event) => handleFormObjectChange(event, index)}
              />
            </FormControl>
          </Collapse>

          {/* EXPAND BUTTON */}
          <Button
            variant="contained"
            disableElevation
            startIcon={
              collapseObject.deliveryPoint ? (
                <IconArrowDropUp />
              ) : (
                <IconArrowDropDown />
              )
            }
            className={classes.formItemButtonExpand}
            onClick={() => handleCollapseChange(index)}
          >
            {collapseObject.deliveryPoint
              ? "Hide full data entry"
              : "Fill in more complete data?"}
          </Button>
        </Box>
      ))}
    </>
  );
};

export default DeliveryPoint;

App.jsx

const App = () => {
  const classes = useStyles();

  const initialFormObject = {
    // DELIVERY POINT
    deliveryPointList: [
      {
        consignee: "",
        deliveryDate: new Date(),
        deliveryInstruction: ""
      }
    ]
    // OTHER OBJECT ITEMS HERE
  };

  const initialCollapseObject = {
    deliveryPointList: [false]
    // OTHER LIST ITEMS HERE
  };

  const [formObject, setFormObject] = useState(initialFormObject);
  const [collapseObject, setCollapseObject] = useState(initialCollapseObject);

  return (
    <Box className={classes.pageRoot}>
      {/* FORM */}
      <Box className={classes.formContainer}>
        {/* DELIVERY POINT */}
        <DeliveryPoint
          initialFormObject={initialFormObject}
          formObject={formObject}
          setFormObject={setFormObject}
          collapseObject={collapseObject}
          setCollapseObject={setCollapseObject}
        />
      </Box>
    </Box>
  );
};

export default App;

Here is the full demo https://codesandbox.io/s/stackoverflow-dynamic-form-wxrmd0

Steps to reproduce:

  1. Click the "Add Delivery" button twice. So there will be 3 delivery point forms.
  2. Change the "consignee" or the "delivery date" in the first delivery point form. Therefore, the second delivery point form will have the same value as the first form.

What’s wrong with my state management and what’s the solution for this?

Note: you can assume the OultinedInput component from Material UI as an HTML input element.

>Solution :

Issue

You are mutating the state in your handleFormObjectChange and handleDatePickerChange handlers.

const handleFormObjectChange = (inputEvent, inputIndex) => {
  let deliveryPointListData = [...formObject.deliveryPointList];

  // Mutates the nested property!!
  deliveryPointListData[inputIndex][inputEvent.target.name] =
    inputEvent.target.value;

  setFormObject((current) => ({
    ...current,
    deliveryPointList: deliveryPointListData
  }));
};

const handleDatePickerChange = (inputNewValue, inputIndex) => {
  let deliveryPointListData = [...formObject.deliveryPointList];

  // Mutates the nested property!!
  deliveryPointListData[inputIndex].deliveryDate = inputNewValue;

  setFormObject((current) => ({
    ...current,
    deliveryPointList: deliveryPointListData
  }));
};

Solution

Ensure you are shallow copying all properties and nested properties that are being updated. This ensures all updates create new object references.

const handleFormObjectChange = (inputEvent, inputIndex) => {
  let deliveryPointListData = [...formObject.deliveryPointList];

  deliveryPointListData[inputIndex] = {
    ...deliveryPointListData[inputIndex], // <-- shallow copy
    [inputEvent.target.name]: inputEvent.target.value
  };

  setFormObject((current) => ({
    ...current,
    deliveryPointList: deliveryPointListData
  }));
};

const handleDatePickerChange = (inputNewValue, inputIndex) => {
  let deliveryPointListData = [...formObject.deliveryPointList];

  deliveryPointListData[inputIndex] = {
    ...deliveryPointListData[inputIndex], // <-- shallow copy
    deliveryDate: inputNewValue,
  };

  setFormObject((current) => ({
    ...current,
    deliveryPointList: deliveryPointListData
  }));
};

Edit dynamic-form-returns-the-same-value-for-the-first-and-second-input

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading