When selecting new data for the dialog form it will incorrectly display the original data. Only after closing the dialog and then reopening the same one, attempted before, does the form display the correct values. I believe there is some useEffect
and useState
shenanigans but not sure how to resolve.
To Test click Button Data 1
to verify data is correct and then click Data 2
. It will show the data corresponding to Data1
not Data 2
where we obviously want to see the Data 2
values.
In the demo, and actual implementation, the selectedId state is needed because it is refernce elsewhere.
import React, { useState, useCallback, useEffect } from "react";
import SomeDialog from "./SomeDialog";
import { FormData } from "./FormData";
import { Button } from "@mui/material";
let initialFormData: FormData[] = [
{ name: "data1", id: 0, location: "here" },
{ name: "data2", id: 1, location: "there" }
];
function Demo(): any {
const [selectedData, setSelectedData] = useState<FormData>(
initialFormData[0]
);
const [selectedId, setSelectedId] = useState<number>(0);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const handleDialogOpen = useCallback(() => setDialogOpen(true), []);
const handleDialogClose = useCallback(() => setDialogOpen(false), []);
const onClick = (index: number) => {
setSelectedId(index);
handleDialogOpen();
};
useEffect(() => {
let data = initialFormData.find((data) => data.id === selectedId);
setSelectedData(data);
}, [selectedId]);
return (
<div style={{ height: "100vh", width: "100vw" }}>
<Button
onClick={() => {
onClick(0);
}}
>
Data 1
</Button>
<Button
onClick={() => {
onClick(1);
}}
>
Data 2
</Button>
<SomeDialog
open={dialogOpen}
onClose={handleDialogClose}
data={selectedData}
/>
</div>
);
}
export default Demo;
Dialog Form
import React, { useState, useEffect, ChangeEvent } from "react";
import TextField from "@mui/material/TextField";
import { Button, Dialog, Box } from "@mui/material";
import { FormData } from "./FormData";
interface SomeDialogProps {
open: boolean;
onClose: () => void;
data: FormData;
}
function SomeDialog(props: SomeDialogProps) {
const { open, onClose, data } = props;
const [formData, setFormData] = useState<FormData>(data);
useEffect(() => {
setFormData(data);
}, [data]);
function handleDataInputChange(event: ChangeEvent<HTMLInputElement>) {
const { id, value } = event.target;
setFormData({ ...formData, [id]: value });
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
console.info(formData);
onClose();
}
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box
component="form"
sx={{
"& .MuiTextField-root": { m: 1, width: "25ch" }
}}
noValidate
autoComplete="off"
onSubmit={handleSubmit}
>
<TextField
id="id"
label="Id"
defaultValue={formData.id}
onChange={handleDataInputChange}
/>
<TextField
id="name"
label="Name"
defaultValue={formData.name}
onChange={handleDataInputChange}
/>
<TextField
id="location"
label="Location"
defaultValue={formData.location}
onChange={handleDataInputChange}
/>
<Button type="submit">Submit</Button>
</Box>
</Dialog>
);
}
export default SomeDialog;
export interface FormData {
name: string;
id: number;
location: string;
}
I see two problems here:
In your Demo.tsx
file, setSelectedData(data)
call in the useEffect
is asynchronous. It means that when you call handleDialogOpen()
immediately after setting selectedId
, the selectedData
state may not have been updated yet, leading to the dialog displaying the old data. Instead, you can combine the logic of both selectedId
and selectedData
in the same function before calling handleDialogOpen()
like this:
const onClick = (index: number) => {
let data = initialFormData.find((data) => data.id === index);
setSelectedData(data);
setSelectedId(index);
handleDialogOpen();
};
This ensures that when you open the dialog, selectedData
is updated with the correct data immediately before opening the dialog.
Similarly, in your SomeDialog.tsx
file, formData
is set to the value of data
prop on the first render. However, as aforementioned, React state updates are asynchronous, which means that when the data
prop changes, the formData
state variable is not updated immediately during the render cycle.
So, if the data
prop changes (for example, when you open the dialog with different data), formData
will still hold the previous value from the previous render. This is because the state update caused by changing the data
prop hasn't had the chance to take effect yet.
To fix this issue, you should directly use the data
prop to initialize the form fields, and you can remove the local formData
state entirely:
import React, { useEffect, ChangeEvent } from "react";
import TextField from "@mui/material/TextField";
import { Button, Dialog, Box } from "@mui/material";
import { FormData } from "./FormData";
interface SomeDialogProps {
open: boolean;
onClose: () => void;
data: FormData;
}
function SomeDialog(props: SomeDialogProps) {
const { open, onClose, data } = props;
function handleDataInputChange(event: ChangeEvent<HTMLInputElement>) {
const { id, value } = event.target;
// No need to update local state, directly use data prop
data[id] = value;
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
console.info(data);
onClose();
}
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box
component="form"
sx={{
"& .MuiTextField-root": { m: 1, width: "25ch" }
}}
noValidate
autoComplete="off"
onSubmit={handleSubmit}
>
<TextField
id="id"
label="Id"
defaultValue={data.id}
onChange={handleDataInputChange}
/>
<TextField
id="name"
label="Name"
defaultValue={data.name}
onChange={handleDataInputChange}
/>
<TextField
id="location"
label="Location"
defaultValue={data.location}
onChange={handleDataInputChange}
/>
<Button type="submit">Submit</Button>
</Box>
</Dialog>
);
}
export default SomeDialog;