I am struggling creating a dynamic form with react-hook-form's useFieldArry
, that appends a new field/input whenever the last field element gets non-empty (so that the user do not have to care about adding more inputs).
While I have a working solution for simple cases (adding data by typing), it fails in the following case: When resetting that form and filling it programmatically with data (e.g. by clicking a button), it appends two empty inputs in the end. First I thought this is related to reacts Strict Mode, but it happens even in the production build.
Any clue why this happens, a solution (or even best practice) to handle this issue?
Here's a minimal working example:
import { useEffect } from "react";
import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form";
interface FormValues {
items: {
text: string;
}[];
}
export default function DynamicForm() {
const { control, reset } = useForm<FormValues>({
defaultValues: {
items: [{ text: "" }],
},
});
const { fields, append } = useFieldArray({
control,
name: "items",
});
const watchItems = useWatch({
control,
name: "items",
});
useEffect(() => {
if (watchItems && watchItems.length > 0) {
const lastItem = watchItems[watchItems.length - 1];
if (lastItem?.text && lastItem.text.trim() !== "") {
append({ text: "" }, { shouldFocus: false });
}
}
}, [watchItems, append]);
const handleInsertData = () => {
reset({
items: [{ text: "X" }],
});
};
return (
<div className="p-6 max-w-md mx-auto bg-white rounded-xl shadow-md">
<button onClick={handleInsertData}>Insert Data</button>
<h2 className="text-xl font-bold mb-4">Dynamic Form</h2>
<form>
<div className="space-y-4">
{fields.map((field, index) => (
<div key={field.id} className="flex items-start space-x-2">
<div className="flex-grow">
<Controller
control={control}
name={`items.${index}.text`}
render={({ field }) => (
<textarea
{...field}
className="w-full p-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter text..."
/>
)}
/>
</div>
</div>
))}
</div>
</form>
</div>
);
}
though i'm not 100% sure about the initial problem, i believe it may stem from the blurb in their docs that says:
useWatch's result is optimised for render phase instead of useEffect's deps, to detect value updates you may want to use an external custom hook for value comparison.
i could be wrong, but suspecting that, i used "watch" instead and was able to create the behavior you wanted. their proposed solution didn't seem to be practical, at least in this case.
// retrieved "watch" from useForm hook
const { control, reset, watch } = useForm<FormValues>({
defaultValues: {
items: [{ text: "" }],
},
});
useEffect(() => {
const { unsubscribe } = watch((values) => {
if (values.items?.length) {
const lastItem = values.items[values.items.length - 1];
if (lastItem?.text && lastItem.text.trim() != '') {
append({ text: '' }, { shouldFocus: false });
}
}
});
return () => { unsubscribe(); };
}, [watch]);
if anyone can confirm the initial cause of the problem, that would be appreciated.