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.

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.
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.
But the rest (third, fourth, and so on) forms are unique as seen in the screenshot below. This is the condition I want.
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:
- Click the "Add Delivery" button twice. So there will be 3 delivery point forms.
- 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
}));
};


