I have a main component here:
// import { useState } from "react";
import {
useForm,
Controller,
FormProvider,
SubmitHandler,
} from "react-hook-form";
// import "./App.css";
import { Box, Button, TextField } from "@mui/material";
import { useState } from "react";
import NestedForm from "./NestedForm";
type Inputs = {
outerInput: string;
};
type InnerInput = {
innerString1: string;
innerString2: string;
};
function App() {
const methods = useForm<Inputs>({
shouldUnregister: true,
});
const [innerInput, setInnerInput] = useState<InnerInput[]>([
{ innerString1: "test1", innerString2: "test2" },
{ innerString1: "test3", innerString2: "test4" },
{ innerString1: "test5", innerString2: "test6" },
{ innerString1: "test7", innerString2: "test8" },
{ innerString1: "test9", innerString2: "test10" },
]);
const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);
function removeItem(idx: number) {
setInnerInput((prevInnerInput) =>
prevInnerInput.filter((_, index) => index !== idx)
);
}
return (
<>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<div className="relative mb-5 flex">
<div className="w-7/12">
<Controller
control={methods.control}
name="outerInput"
defaultValue=""
render={({ field, fieldState }) => (
<TextField
label="Outer Input"
error={!!fieldState.error}
helperText={fieldState.error?.message}
{...field}
fullWidth
variant="standard"
type="text"
/>
)}
/>
</div>
{innerInput.map((item, idx) => (
<NestedForm
key={`inner-${idx}`}
idx={idx}
data={item}
handleRemove={() => removeItem(idx)}
/>
))}
</div>
<Box width={"20%"} display="flex" justifyContent={"center"}>
<Button type="submit">Save</Button>
</Box>
</form>
</FormProvider>
</>
);
}
export default App;
And a nested form with two text fields:
import { Box, Button, TextField } from "@mui/material";
import { useFormContext } from "react-hook-form";
type InnerInput = {
innerString1: string;
innerString2: string;
};
function NestedForm({
idx,
data,
handleRemove,
}: {
idx: number;
data: InnerInput;
handleRemove: () => void;
}) {
const { register } = useFormContext();
return (
<div>
<TextField
{...register(`inner[${idx}.innerString1`)}
label="Inner Input 1"
defaultValue={data.innerString1}
fullWidth
variant="standard"
type="text"
/>
<TextField
{...register(`inner[${idx}.innerString2`)}
defaultValue={data.innerString2}
label="Inner Input 2"
fullWidth
variant="standard"
type="text"
/>
<Box width={"20%"} display="flex" justifyContent={"center"}>
<Button onClick={handleRemove}>Remove</Button>
</Box>
</div>
);
}
export default NestedForm;
I am getting a strange behavior.
When I click on Remove
on the first item, this will remove the last each time I click on the Remove
, and finally when none left this will remove the first item.
But still when I click on Submit, I can still see all the items of the innerInput
CodeSandbox: repro
I found your problem and fixed it: https://codesandbox.io/p/devbox/mui-react-form-forked-69s8rf?file=%2Fsrc%2FApp.tsx%3A40%2C44&workspaceId=117705be-c8bb-48c0-8ae7-a3443e7260ba
What was your problem?
You used the array index as an id
for elements inside an array, which is never a good idea because the ID is a unique value; an array index is not. If you delete an element inside an array, the index moves, nut the ID will stay the same.
How did I fix it?
First, I changed the Input Objects to have an extra property, id
. For convenience, I used the numbers from 0 to 4, which you should change to a uuid or something else.
I changed the filter function to verify the item.id
instead of the index.
function removeItem(idx: number) {
setInnerInput((prevInnerInput) =>
prevInnerInput.filter((item) => item.id !== idx),
);
}
Lastly I changed the map function to use the item.id
instead of the index.
{innerInput.map((item) => (
<NestedForm
key={`inner-${item.id}`}
idx={item.id}
data={item}
handleRemove={() => removeItem(item.id)}
/>
))}
In general, it is better for objects inside arrays that you manipulate to have an id
and not use an index.